fun_ci 1.0.0 → 1.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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +3 -1
  4. data/exe/fun-ci-trigger +6 -6
  5. data/exe/fun-ci-tui +9 -4
  6. data/lib/fun_ci/animations/celebrate.rb +210 -0
  7. data/lib/fun_ci/animations/explosion.rb +138 -0
  8. data/lib/fun_ci/animations/flash.rb +205 -0
  9. data/lib/fun_ci/animations/idle.rb +127 -0
  10. data/lib/fun_ci/animations/leprechauns.rb +240 -0
  11. data/lib/fun_ci/animations/running.rb +129 -0
  12. data/lib/fun_ci/animations/success.rb +281 -0
  13. data/lib/fun_ci/animations/yay.rb +169 -0
  14. data/lib/fun_ci/cli.rb +20 -14
  15. data/lib/fun_ci/persistence/database.rb +52 -0
  16. data/lib/fun_ci/persistence/pipeline_recorder.rb +74 -0
  17. data/lib/fun_ci/persistence/pipeline_run.rb +59 -0
  18. data/lib/fun_ci/persistence/stage_job.rb +46 -0
  19. data/lib/fun_ci/{state_machine.rb → persistence/state_machine.rb} +3 -12
  20. data/lib/fun_ci/pipeline/background_wrapper.rb +27 -0
  21. data/lib/fun_ci/pipeline/pipeline_forker.rb +38 -0
  22. data/lib/fun_ci/pipeline/process_runner.rb +28 -0
  23. data/lib/fun_ci/pipeline/progress_reporter.rb +31 -0
  24. data/lib/fun_ci/pipeline/stage_runner.rb +85 -0
  25. data/lib/fun_ci/pipeline/stale_pipeline_canceller.rb +53 -0
  26. data/lib/fun_ci/pipeline/trigger.rb +153 -0
  27. data/lib/fun_ci/setup/hook_writer.rb +75 -0
  28. data/lib/fun_ci/setup/installer.rb +55 -0
  29. data/lib/fun_ci/setup/maven_linter_detector.rb +26 -0
  30. data/lib/fun_ci/setup/project_config.rb +42 -0
  31. data/lib/fun_ci/setup/project_detector.rb +22 -0
  32. data/lib/fun_ci/setup/setup_checker.rb +30 -0
  33. data/lib/fun_ci/setup/template_writer.rb +53 -0
  34. data/lib/fun_ci/tui/admin_tui.rb +90 -0
  35. data/lib/fun_ci/tui/animation.rb +49 -0
  36. data/lib/fun_ci/tui/animation_compositor.rb +107 -0
  37. data/lib/fun_ci/tui/animation_frames.rb +112 -0
  38. data/lib/fun_ci/tui/animation_library.rb +46 -0
  39. data/lib/fun_ci/tui/animation_renderer.rb +144 -0
  40. data/lib/fun_ci/tui/ansi.rb +34 -0
  41. data/lib/fun_ci/tui/board_data.rb +53 -0
  42. data/lib/fun_ci/tui/board_renderer.rb +105 -0
  43. data/lib/fun_ci/tui/duration_formatter.rb +24 -0
  44. data/lib/fun_ci/tui/header_animation_manager.rb +71 -0
  45. data/lib/fun_ci/tui/header_animation_player.rb +45 -0
  46. data/lib/fun_ci/tui/key_handler.rb +86 -0
  47. data/lib/fun_ci/tui/looping_animation_player.rb +45 -0
  48. data/lib/fun_ci/tui/relative_time.rb +22 -0
  49. data/lib/fun_ci/tui/row_formatter.rb +108 -0
  50. data/lib/fun_ci/tui/screen.rb +103 -0
  51. data/lib/fun_ci/tui/spinner.rb +24 -0
  52. data/lib/fun_ci/tui/stage_change_detector.rb +58 -0
  53. data/lib/fun_ci/tui/streak_counter.rb +29 -0
  54. data/lib/fun_ci/tui/terminal_input.rb +69 -0
  55. data/lib/fun_ci.rb +6 -6
  56. metadata +49 -28
  57. data/lib/fun_ci/admin_tui.rb +0 -226
  58. data/lib/fun_ci/ansi.rb +0 -21
  59. data/lib/fun_ci/background_wrapper.rb +0 -27
  60. data/lib/fun_ci/board_data.rb +0 -51
  61. data/lib/fun_ci/database.rb +0 -50
  62. data/lib/fun_ci/duration_formatter.rb +0 -23
  63. data/lib/fun_ci/hook_writer.rb +0 -73
  64. data/lib/fun_ci/installer.rb +0 -53
  65. data/lib/fun_ci/maven_linter_detector.rb +0 -24
  66. data/lib/fun_ci/pipeline_forker.rb +0 -36
  67. data/lib/fun_ci/pipeline_recorder.rb +0 -72
  68. data/lib/fun_ci/pipeline_run.rb +0 -57
  69. data/lib/fun_ci/progress_reporter.rb +0 -29
  70. data/lib/fun_ci/project_config.rb +0 -40
  71. data/lib/fun_ci/project_detector.rb +0 -18
  72. data/lib/fun_ci/relative_time.rb +0 -20
  73. data/lib/fun_ci/row_formatter.rb +0 -106
  74. data/lib/fun_ci/screen.rb +0 -81
  75. data/lib/fun_ci/setup_checker.rb +0 -28
  76. data/lib/fun_ci/spinner.rb +0 -22
  77. data/lib/fun_ci/stage_job.rb +0 -44
  78. data/lib/fun_ci/stage_runner.rb +0 -108
  79. data/lib/fun_ci/stale_pipeline_canceller.rb +0 -51
  80. data/lib/fun_ci/streak_counter.rb +0 -30
  81. data/lib/fun_ci/template_writer.rb +0 -51
  82. data/lib/fun_ci/trigger.rb +0 -150
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ansi"
4
+ require_relative "duration_formatter"
5
+ require_relative "relative_time"
6
+
7
+ module FunCi
8
+ module Tui
9
+ module RowFormatter
10
+ STAGE_NAMES = { "lint" => "Lint", "build" => "Build", "fast" => "Fast", "slow" => "Slow" }.freeze
11
+ STATUS_LABELS = {
12
+ "completed" => "PASSED", "failed" => "FAILED", "timed_out" => "TIMED OUT",
13
+ "running" => "RUNNING", "scheduled" => "Scheduled...", "cancelled" => "CANCELLED"
14
+ }.freeze
15
+ PROJECT_COLORS = %w[31 32 33 34 35 36 91 92 93 94].freeze
16
+
17
+ def self.format(run, now: Time.now, spinner_frame: nil, elapsed_seconds: nil)
18
+ commit = run[:commit_hash][0, 7]
19
+ branch = run[:branch]
20
+ status = run[:status]
21
+ project = format_project(run[:project_path])
22
+
23
+ case status
24
+ when "scheduled"
25
+ format_scheduled(commit, branch, project, run[:updated_at], now)
26
+ when "cancelled"
27
+ format_cancelled(commit, branch, project, run[:stages], run[:updated_at], now)
28
+ else
29
+ stages_text = format_stages(run[:stages], status, spinner_frame, elapsed_seconds)
30
+ status_text = format_status(status)
31
+ time_text = Ansi.dim(RelativeTime.format(run[:updated_at], now: now))
32
+ " #{commit} #{branch}#{project} #{stages_text} #{status_text} #{time_text}"
33
+ end
34
+ end
35
+
36
+ def self.format_project(project_path)
37
+ return "" unless project_path
38
+ name = File.basename(project_path)
39
+ code = PROJECT_COLORS[name.hash.abs % PROJECT_COLORS.size]
40
+ " \e[#{code}m#{name}#{Ansi::RESET}"
41
+ end
42
+ private_class_method :format_project
43
+
44
+ def self.format_scheduled(commit, branch, project, updated_at, now)
45
+ time_text = RelativeTime.format(updated_at, now: now)
46
+ Ansi.dim(" #{commit} #{branch}#{project} Scheduled... #{time_text}")
47
+ end
48
+ private_class_method :format_scheduled
49
+
50
+ def self.format_cancelled(commit, branch, project, stages, updated_at, now)
51
+ stages_text = stages.map { |s| format_cancelled_stage(s) }.join(" ")
52
+ time_text = RelativeTime.format(updated_at, now: now)
53
+ Ansi.dim(" #{commit} #{branch}#{project} #{stages_text} CANCELLED #{time_text}")
54
+ end
55
+ private_class_method :format_cancelled
56
+
57
+ def self.format_cancelled_stage(stage)
58
+ name = STAGE_NAMES[stage[:stage]]
59
+ if stage[:duration]
60
+ "#{name} #{DurationFormatter.format(stage[:duration])}"
61
+ else
62
+ "#{name} --"
63
+ end
64
+ end
65
+ private_class_method :format_cancelled_stage
66
+
67
+ def self.format_stages(stages, run_status, spinner_frame, elapsed_seconds)
68
+ stages.map { |s| format_stage(s, spinner_frame, elapsed_seconds) }.join(" ")
69
+ end
70
+ private_class_method :format_stages
71
+
72
+ def self.format_stage(stage, spinner_frame, elapsed_seconds)
73
+ name = STAGE_NAMES[stage[:stage]]
74
+
75
+ case stage[:status]
76
+ when "completed"
77
+ time = DurationFormatter.format(stage[:duration])
78
+ Ansi.green("#{name} #{time}")
79
+ when "failed"
80
+ time = DurationFormatter.format(stage[:duration])
81
+ Ansi.bold_red("#{name} FAIL #{time}")
82
+ when "timed_out"
83
+ time = DurationFormatter.format(stage[:duration])
84
+ Ansi.bold_yellow("#{name} TIMEOUT #{time}")
85
+ when "running"
86
+ frame = spinner_frame || "\u2800"
87
+ time = elapsed_seconds ? "#{elapsed_seconds.to_i}s" : "--"
88
+ Ansi.cyan("#{name} #{frame} #{time}")
89
+ else
90
+ Ansi.dim("#{name} --")
91
+ end
92
+ end
93
+ private_class_method :format_stage
94
+
95
+ def self.format_status(status)
96
+ label = STATUS_LABELS[status]
97
+ case status
98
+ when "completed" then Ansi.bold_green(label)
99
+ when "failed" then Ansi.bold_red(label)
100
+ when "timed_out" then Ansi.bold_yellow(label)
101
+ when "running" then Ansi.bold_cyan(label)
102
+ else Ansi.dim(label)
103
+ end
104
+ end
105
+ private_class_method :format_status
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ansi"
4
+
5
+ module FunCi
6
+ module Tui
7
+ class Screen
8
+ attr_reader :width, :height
9
+
10
+ def initialize(output: $stdout, width: 80)
11
+ @output = output
12
+ @width = width
13
+ end
14
+
15
+ def width=(new_width)
16
+ return if new_width == @width
17
+
18
+ clear
19
+ @width = new_width
20
+ end
21
+
22
+ def height=(new_height)
23
+ return if new_height == @height
24
+
25
+ clear
26
+ @height = new_height
27
+ end
28
+
29
+ def render_header(streak_text:)
30
+ title = " fun-ci"
31
+ if streak_text
32
+ streak_display = Ansi.green(streak_text)
33
+ plain_streak = streak_text
34
+ padding = @width - title.length - plain_streak.length
35
+ padding = 1 if padding < 1
36
+ line = "#{title}#{" " * padding}#{streak_display}"
37
+ else
38
+ line = title.ljust(@width)
39
+ end
40
+ println Ansi.bg_charcoal(Ansi.white(line))
41
+ end
42
+
43
+ def render_footer(empty: false, confirming: false)
44
+ if confirming
45
+ println Ansi.dim(" Cancel running pipeline? y/n")
46
+ elsif empty
47
+ println Ansi.dim(" q quit")
48
+ else
49
+ println Ansi.dim(" j/k move c cancel q quit")
50
+ end
51
+ end
52
+
53
+ def render_empty_state
54
+ println ""
55
+ println ""
56
+ println " No runs yet."
57
+ println ""
58
+ println " Trigger one: fun-ci trigger HEAD"
59
+ println " Or hook it: fun-ci install-hook pre-push"
60
+ println ""
61
+ end
62
+
63
+ def render_board(rows, cursor_index:)
64
+ rows.each_with_index do |row, i|
65
+ if cursor_index == i
66
+ println "> #{row.lstrip}"
67
+ else
68
+ println row
69
+ end
70
+ println unless i == rows.length - 1
71
+ end
72
+ end
73
+
74
+ def println(text = "")
75
+ @output.print "#{text}\e[K\r\n"
76
+ end
77
+
78
+ def clear
79
+ @output.print "\e[2J\e[H"
80
+ end
81
+
82
+ def move_cursor_home
83
+ @output.print "\e[H"
84
+ end
85
+
86
+ def clear_below
87
+ @output.print "\e[J"
88
+ end
89
+
90
+ def write_at(row, col, text)
91
+ @output.print "\e[#{row};#{col}H#{text}"
92
+ end
93
+
94
+ def save_cursor
95
+ @output.print "\e[s"
96
+ end
97
+
98
+ def restore_cursor
99
+ @output.print "\e[u"
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunCi
4
+ module Tui
5
+ class Spinner
6
+ BRAILLE_FRAMES = %W[\u2800 \u2801 \u2803 \u2807 \u280F \u281F \u283F \u287F].freeze
7
+
8
+ attr_reader :frames
9
+
10
+ def initialize
11
+ @frames = BRAILLE_FRAMES
12
+ @index = 0
13
+ end
14
+
15
+ def current_frame
16
+ @frames[@index]
17
+ end
18
+
19
+ def advance!
20
+ @index = (@index + 1) % @frames.length
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunCi
4
+ module Tui
5
+ module StageChangeDetector
6
+ Change = Data.define(:run_id, :stage, :from, :to)
7
+
8
+ def self.detect(previous_runs, current_runs)
9
+ return [] if previous_runs.empty?
10
+
11
+ current_by_id = index_by_id(current_runs)
12
+ changes = []
13
+
14
+ previous_runs.each do |prev_run|
15
+ cur_run = current_by_id[prev_run[:id]]
16
+ next unless cur_run
17
+
18
+ changes.concat(compare_stages(prev_run, cur_run))
19
+ end
20
+
21
+ changes
22
+ end
23
+
24
+ def self.index_by_id(runs)
25
+ runs.each_with_object({}) { |r, h| h[r[:id]] = r }
26
+ end
27
+ private_class_method :index_by_id
28
+
29
+ def self.compare_stages(prev_run, cur_run)
30
+ prev_stages = index_stages(prev_run[:stages])
31
+ changes = []
32
+
33
+ cur_run[:stages].each do |cur_stage|
34
+ prev_stage = prev_stages[cur_stage[:stage]]
35
+ prev_status = prev_stage ? prev_stage[:status] : nil
36
+ next if prev_status == cur_stage[:status]
37
+
38
+ changes << Change.new(
39
+ run_id: cur_run[:id],
40
+ stage: cur_stage[:stage],
41
+ from: prev_status,
42
+ to: cur_stage[:status]
43
+ )
44
+ end
45
+
46
+ changes
47
+ end
48
+ private_class_method :compare_stages
49
+
50
+ def self.index_stages(stages)
51
+ return {} unless stages
52
+
53
+ stages.each_with_object({}) { |s, h| h[s[:stage]] = s }
54
+ end
55
+ private_class_method :index_stages
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunCi
4
+ module Tui
5
+ module StreakCounter
6
+ PASS_STATUS = "completed"
7
+ ACTIVE_STATUSES = %w[running scheduled].freeze
8
+
9
+ def self.count(runs)
10
+ terminal_runs = runs.reject { |r| ACTIVE_STATUSES.include?(r[:status]) }
11
+ return nil if terminal_runs.empty?
12
+
13
+ streak = 0
14
+ terminal_runs.each do |run|
15
+ break unless run[:status] == PASS_STATUS
16
+ streak += 1
17
+ end
18
+ streak
19
+ end
20
+
21
+ def self.format_text(streak)
22
+ return nil if streak.nil?
23
+ return "Streak broken" if streak == 0
24
+
25
+ "#{streak} in a row!"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FunCi
4
+ module Tui
5
+ class TerminalInput
6
+ def initialize(input:, width_provider: nil)
7
+ @input = input
8
+ @width_provider = width_provider
9
+ @signal_read = nil
10
+ @signal_write = nil
11
+ end
12
+
13
+ def setup_signal_trap
14
+ return unless @width_provider
15
+
16
+ @signal_read, @signal_write = IO.pipe
17
+ Signal.trap("WINCH") do
18
+ @signal_write&.write_nonblock(".") rescue nil # rubocop:disable Style/RescueModifier
19
+ end
20
+ end
21
+
22
+ def check_width
23
+ return nil unless @width_provider
24
+
25
+ @width_provider.call
26
+ end
27
+
28
+ def setup_raw_mode
29
+ @input.raw! if @input.respond_to?(:raw!)
30
+ rescue Errno::ENOTTY
31
+ # Not a terminal (testing)
32
+ end
33
+
34
+ def restore_terminal
35
+ @input.cooked! if @input.respond_to?(:cooked!)
36
+ rescue Errno::ENOTTY
37
+ # Not a terminal
38
+ end
39
+
40
+ def read_key_with_timeout(timeout)
41
+ return nil unless @input.respond_to?(:read_nonblock)
42
+
43
+ watched = [@input]
44
+ watched << @signal_read if @signal_read
45
+ ready = IO.select(watched, nil, nil, timeout)
46
+ return nil unless ready
47
+
48
+ if @signal_read && ready[0].include?(@signal_read)
49
+ @signal_read.read_nonblock(1) rescue nil # rubocop:disable Style/RescueModifier
50
+ return nil unless ready[0].include?(@input)
51
+ end
52
+
53
+ byte = @input.read_nonblock(1)
54
+ if byte == "\e"
55
+ seq = @input.read_nonblock(2) rescue "" # rubocop:disable Style/RescueModifier
56
+ case seq
57
+ when "[A" then :up
58
+ when "[B" then :down
59
+ else :escape
60
+ end
61
+ else
62
+ byte
63
+ end
64
+ rescue IO::WaitReadable, EOFError
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
data/lib/fun_ci.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "fun_ci/database"
4
- require_relative "fun_ci/state_machine"
5
- require_relative "fun_ci/pipeline_run"
6
- require_relative "fun_ci/stage_job"
7
- require_relative "fun_ci/project_config"
3
+ require_relative "fun_ci/persistence/database"
4
+ require_relative "fun_ci/persistence/state_machine"
5
+ require_relative "fun_ci/persistence/pipeline_run"
6
+ require_relative "fun_ci/persistence/stage_job"
7
+ require_relative "fun_ci/setup/project_config"
8
8
 
9
9
  module FunCi
10
- VERSION = "1.0.0"
10
+ VERSION = "1.2.0"
11
11
 
12
12
  BUILD_TIMEOUT = 30
13
13
  FAST_SUITE_TIMEOUT = 10
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fun_ci
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Thyge Madsen
@@ -41,34 +41,55 @@ files:
41
41
  - exe/fun-ci-trigger
42
42
  - exe/fun-ci-tui
43
43
  - lib/fun_ci.rb
44
- - lib/fun_ci/admin_tui.rb
45
- - lib/fun_ci/ansi.rb
46
- - lib/fun_ci/background_wrapper.rb
47
- - lib/fun_ci/board_data.rb
44
+ - lib/fun_ci/animations/celebrate.rb
45
+ - lib/fun_ci/animations/explosion.rb
46
+ - lib/fun_ci/animations/flash.rb
47
+ - lib/fun_ci/animations/idle.rb
48
+ - lib/fun_ci/animations/leprechauns.rb
49
+ - lib/fun_ci/animations/running.rb
50
+ - lib/fun_ci/animations/success.rb
51
+ - lib/fun_ci/animations/yay.rb
48
52
  - lib/fun_ci/cli.rb
49
- - lib/fun_ci/database.rb
50
- - lib/fun_ci/duration_formatter.rb
51
- - lib/fun_ci/hook_writer.rb
52
- - lib/fun_ci/installer.rb
53
- - lib/fun_ci/maven_linter_detector.rb
54
- - lib/fun_ci/pipeline_forker.rb
55
- - lib/fun_ci/pipeline_recorder.rb
56
- - lib/fun_ci/pipeline_run.rb
57
- - lib/fun_ci/progress_reporter.rb
58
- - lib/fun_ci/project_config.rb
59
- - lib/fun_ci/project_detector.rb
60
- - lib/fun_ci/relative_time.rb
61
- - lib/fun_ci/row_formatter.rb
62
- - lib/fun_ci/screen.rb
63
- - lib/fun_ci/setup_checker.rb
64
- - lib/fun_ci/spinner.rb
65
- - lib/fun_ci/stage_job.rb
66
- - lib/fun_ci/stage_runner.rb
67
- - lib/fun_ci/stale_pipeline_canceller.rb
68
- - lib/fun_ci/state_machine.rb
69
- - lib/fun_ci/streak_counter.rb
70
- - lib/fun_ci/template_writer.rb
71
- - lib/fun_ci/trigger.rb
53
+ - lib/fun_ci/persistence/database.rb
54
+ - lib/fun_ci/persistence/pipeline_recorder.rb
55
+ - lib/fun_ci/persistence/pipeline_run.rb
56
+ - lib/fun_ci/persistence/stage_job.rb
57
+ - lib/fun_ci/persistence/state_machine.rb
58
+ - lib/fun_ci/pipeline/background_wrapper.rb
59
+ - lib/fun_ci/pipeline/pipeline_forker.rb
60
+ - lib/fun_ci/pipeline/process_runner.rb
61
+ - lib/fun_ci/pipeline/progress_reporter.rb
62
+ - lib/fun_ci/pipeline/stage_runner.rb
63
+ - lib/fun_ci/pipeline/stale_pipeline_canceller.rb
64
+ - lib/fun_ci/pipeline/trigger.rb
65
+ - lib/fun_ci/setup/hook_writer.rb
66
+ - lib/fun_ci/setup/installer.rb
67
+ - lib/fun_ci/setup/maven_linter_detector.rb
68
+ - lib/fun_ci/setup/project_config.rb
69
+ - lib/fun_ci/setup/project_detector.rb
70
+ - lib/fun_ci/setup/setup_checker.rb
71
+ - lib/fun_ci/setup/template_writer.rb
72
+ - lib/fun_ci/tui/admin_tui.rb
73
+ - lib/fun_ci/tui/animation.rb
74
+ - lib/fun_ci/tui/animation_compositor.rb
75
+ - lib/fun_ci/tui/animation_frames.rb
76
+ - lib/fun_ci/tui/animation_library.rb
77
+ - lib/fun_ci/tui/animation_renderer.rb
78
+ - lib/fun_ci/tui/ansi.rb
79
+ - lib/fun_ci/tui/board_data.rb
80
+ - lib/fun_ci/tui/board_renderer.rb
81
+ - lib/fun_ci/tui/duration_formatter.rb
82
+ - lib/fun_ci/tui/header_animation_manager.rb
83
+ - lib/fun_ci/tui/header_animation_player.rb
84
+ - lib/fun_ci/tui/key_handler.rb
85
+ - lib/fun_ci/tui/looping_animation_player.rb
86
+ - lib/fun_ci/tui/relative_time.rb
87
+ - lib/fun_ci/tui/row_formatter.rb
88
+ - lib/fun_ci/tui/screen.rb
89
+ - lib/fun_ci/tui/spinner.rb
90
+ - lib/fun_ci/tui/stage_change_detector.rb
91
+ - lib/fun_ci/tui/streak_counter.rb
92
+ - lib/fun_ci/tui/terminal_input.rb
72
93
  homepage: https://github.com/beatmadsen/fun-ci
73
94
  licenses:
74
95
  - MIT