guard-status 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8d1d43c035152dcc6fe5d7e944e429a6ffb1503099b12b5602a4020b279e9c86
4
+ data.tar.gz: f9718158df1ba30f57587dc3afa499a32938847d80caf5abe0b9167dcf9a092e
5
+ SHA512:
6
+ metadata.gz: e09f1996627a242367636170748283fa3394e528636cfa9a4bc2c5ea8b1a69c0ad110c77900463e045790c0e422ce64c8818dd363f92b0d730060141c464f3e8
7
+ data.tar.gz: 742ad2486c3e563f8ba6ccec669ec740e60a6a784833b40452436c06f8d6acad98def94d2eae8d76e02d5287eadf4ec0e3e1929f47ab22787300bf7105164ac2
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-10-26
4
+
5
+ ### ✨ Features
6
+ - **`guard :status` plugin:** prints one status line after the other guards have run.
7
+ - **`rubocop:` and `rspec:` segments:** read the JSON reports of those tools.
8
+ - **`file:` segments:** read `<state> <args>` status files, with `states:` for custom states.
9
+ - **`title`, `prefix`, `title_color` and `notify` options.**
10
+ - **Print only when needed:** prints after a change only when another guard has run for it, learned from Guard's callbacks instead of the changed files.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ "guard-status" follows [The Ruby Community Conduct Guideline](https://www.ruby-lang.org/en/conduct) in all "collaborative space", which is defined as community communications channels (such as mailing lists, submitted patches, commit comments, etc.):
4
+
5
+ * Participants will be tolerant of opposing views.
6
+ * Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
7
+ * When interpreting the words and actions of others, participants should always assume good intentions.
8
+ * Behaviour which can be reasonably considered harassment will not be tolerated.
9
+
10
+ If you have any concerns about behaviour within this project, please contact us at ["joesharp13@gmail.com"](mailto:"joesharp13@gmail.com").
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Joe Sharp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # guard-status
2
+
3
+ A one line status to complete your Guard output. Ideal when you have more than one Guard Plugin.
4
+
5
+ ![🔮 Status: RuboCop: ✅ | Tests: ❌ (2) | Docs: ✅ | Bench: 🐢 (0.47s vs 0.32s)](docs/status.svg)
6
+
7
+ ## Setup
8
+
9
+ ```ruby
10
+ # Gemfile
11
+ group :development do
12
+ gem 'guard-status'
13
+ end
14
+ ```
15
+
16
+ ```sh
17
+ bundle exec guard init status # adds `guard :status` to your Guardfile
18
+ bundle exec guard
19
+ ```
20
+
21
+ > [!IMPORTANT]
22
+ > **Put `guard :status` last in your Guardfile.** Guard runs plugins in the order they appear, so the last one
23
+ > sees what the others just recorded.
24
+
25
+ ## Segments
26
+
27
+ Each segment is a label and where its result comes from. They are shown in the order given:
28
+
29
+ ```ruby
30
+ guard :status, segments: {
31
+ 'RuboCop' => { rubocop: 'tmp/rubocop.json' },
32
+ 'Tests' => { rspec: 'tmp/rspec.json' },
33
+ 'Docs' => 'tmp/docs_status.txt',
34
+ 'Bench' => { file: 'tmp/benchmark_status.txt',
35
+ states: { slow: { display: '🐢 (%{1}s vs %{2}s)', severity: :failed } } }
36
+ }
37
+ ```
38
+
39
+ Without `segments`, the RuboCop and Tests segments above are shown.
40
+
41
+ ### RuboCop and RSpec
42
+
43
+ These read the JSON reports of the tools, so the other guards need to write them. Put `--format json --out <path>`
44
+ after any other format:
45
+
46
+ ```ruby
47
+ guard :rubocop, cli: '--format progress --format json --out tmp/rubocop.json'
48
+ guard :rspec, cmd: 'bundle exec rspec --format progress --format json --out tmp/rspec.json'
49
+ ```
50
+
51
+ | Segment | Shows |
52
+ |---|---|
53
+ | no problems | ✅ |
54
+ | problems | ⚠️ (3) offenses for RuboCop, ❌ (2) failures for RSpec (errors outside examples count) |
55
+ | no report yet | … |
56
+ | a report that can't be read | ? |
57
+
58
+ ### Status files
59
+
60
+ A string, or `file:`, reads a status file. The first word is the state and the rest are its arguments:
61
+
62
+ ```
63
+ ok
64
+ slow 0.47 0.32
65
+ error something went wrong
66
+ ```
67
+
68
+ [guard-boondoc](https://github.com/joe-sharp/guard-boondoc) writes this format with its `status_file` option, and
69
+ any script can write it.
70
+
71
+ | State | Shows | Severity |
72
+ |---|---|---|
73
+ | `ok` | ✅ | success |
74
+ | `error` | ❌ | failed |
75
+ | an empty or missing file | … | pending |
76
+ | anything not declared | `? <state>` | failed |
77
+ | a file that can't be read | `?` | failed |
78
+
79
+ Control characters in the file, such as terminal escape sequences, are treated as spaces.
80
+
81
+ ### Your own states
82
+
83
+ Declare a state with the `states:` option of a `file:` segment. `display` is what to show, in which `%{1}`, `%{2}`
84
+ and so on stand for the arguments after the state, counting from 1, and `severity` is one of `:success`,
85
+ `:pending` or `:failed`:
86
+
87
+ ```ruby
88
+ 'Card' => { file: 'tmp/card_status.txt',
89
+ states: { changed: { display: '🔄', severity: :failed } } }
90
+ ```
91
+
92
+ Declaring `ok` or `error` replaces the built-in. A typo in a status file shows up as `? slwo` instead of passing
93
+ silently. A state without a `display` or a valid `severity`, or a display that uses `%{0}`, raises an
94
+ `ArgumentError` when Guard starts.
95
+
96
+ ## Options
97
+
98
+ | Option | Default | |
99
+ |---|---|---|
100
+ | `segments` | RuboCop and Tests | the labels and their sources, in order |
101
+ | `title` | `'Status'` | |
102
+ | `prefix` | `'🔮'` | shown before the title |
103
+ | `title_color` | `93` | `:black`, `:red`, `:green`, `:yellow`, `:blue`, `:magenta`, `:cyan` or `:white` (0 to 7), a 256-color number from 0 to 255, or `nil` for none. The title is plain when stdout is not a terminal or `NO_COLOR` is set |
104
+ | `notify` | `false` | also send the summary as a Guard notification, with the worst severity as its image |
105
+
106
+ For example, to change the title, prefix and title color:
107
+
108
+ ```ruby
109
+ guard :status, title: 'Guard Status', prefix: '💻', title_color: :green
110
+ ```
111
+
112
+ ![💻 Guard Status: RuboCop: ✅ | Tests: ✅](docs/modified-status.svg)
113
+
114
+ To see the 256 colors and pick a number for `title_color`, run `bundle exec guard-status-colors`.
115
+
116
+ > [!IMPORTANT]
117
+ > **Always put quotes around emoji** (`prefix: '💻'`, not `prefix: 💻`). The Guardfile is Ruby, and an unquoted
118
+ > emoji is parsed as a variable or method name, so it will raise an error instead of printing.
119
+
120
+ ## When it prints
121
+
122
+ On start, reload and run all, and again whenever another guard starts a run in response to a change. It learns
123
+ this from the callbacks Guard fires before each task (`run_on_modifications_begin` and the like), not from the files
124
+ that changed, so a file nobody is watching, or one a tool writes as a side effect such as `.rspec_status`, prints
125
+ nothing. A guard that goes on to fail still counts, since the callback fires before the run. One batch of changes
126
+ prints one line, however many guards ran.
127
+
128
+ RuboCop and RSpec only run the changed files when a file changes, and overwrite their reports, so those segments
129
+ count just those files until a full run, such as `all_after_pass`, writes the report again.
130
+
131
+ The plugin only reports, it never fails a Guard task. Because it prints from its own turn in the batch, it has to
132
+ come after the guards it reports on, as described in Setup. Any `watch` lines you give it narrow which batches
133
+ reach it.
134
+
135
+ ## Development
136
+
137
+ ```sh
138
+ bundle exec rake # specs and RuboCop
139
+ ```
140
+
141
+ ## License
142
+
143
+ MIT, see [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Prints the 256 colors a `title_color:` number can pick, each number in its own color, 20 to a row.
5
+ COLUMNS = 20
6
+
7
+ (0..255).each_slice(COLUMNS) do |row|
8
+ puts row.map { |number| "\e[38;5;#{number}m#{number.to_s.rjust(3)}\e[0m" }.join(' ')
9
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'guard/compat/plugin'
5
+ require_relative 'result'
6
+
7
+ module Guard
8
+ class Status < Plugin
9
+ # Reads a count of problems from the `summary` of a tool's JSON report. Subclasses say how to find the count in it
10
+ # and how to show it.
11
+ class JsonReader
12
+ attr_reader :path
13
+
14
+ # @param path [String] the JSON report the tool writes
15
+ def initialize(path)
16
+ @path = path
17
+ end
18
+
19
+ # @return [Result]
20
+ def result
21
+ summary = JSON.parse(File.read(path))
22
+ summary = summary['summary'] if summary.is_a?(Hash)
23
+ return Result.unknown unless summary.is_a?(Hash)
24
+
25
+ count = count(summary)
26
+ return Result.unknown unless count.is_a?(Integer)
27
+
28
+ count.zero? ? Result.success : failure(count)
29
+ rescue Errno::ENOENT
30
+ Result.pending
31
+ rescue JSON::ParserError, SystemCallError
32
+ Result.unknown
33
+ end
34
+
35
+ private
36
+
37
+ def count(_summary) = raise(NotImplementedError)
38
+ def failure(_count) = raise(NotImplementedError)
39
+ end
40
+
41
+ # RuboCop's offenses, from `--format json --out <path>`.
42
+ class RubocopReader < JsonReader
43
+ private
44
+
45
+ def count(summary) = summary['offense_count']
46
+ def failure(count) = Result.new(:failed, "⚠️ (#{count})")
47
+ end
48
+
49
+ # RSpec's failures, including errors raised outside any example, from `--format json --out <path>`.
50
+ class RspecReader < JsonReader
51
+ private
52
+
53
+ def count(summary)
54
+ failures = summary['failure_count']
55
+ errors = summary['errors_outside_of_examples_count'] || 0
56
+ failures + errors if failures.is_a?(Integer) && errors.is_a?(Integer)
57
+ end
58
+
59
+ def failure(count) = Result.new(:failed, "❌ (#{count})")
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'guard/compat/plugin'
4
+ require_relative 'result'
5
+
6
+ module Guard
7
+ class Status < Plugin
8
+ # The status line made from each segment's result.
9
+ class Line
10
+ # The names are the first eight of the 256 colors, so a name and its number are the same color.
11
+ COLORS = { black: 0, red: 1, green: 2, yellow: 3, blue: 4, magenta: 5, cyan: 6, white: 7 }.freeze
12
+
13
+ # @param results [Hash{String => Result}] in the order to show them
14
+ # @param title [String]
15
+ # @param prefix [String] shown before the title
16
+ # @param color [Symbol, Integer, nil] one of {COLORS}, a 256-color number from 0 to 255, or nil to leave the
17
+ # title uncolored
18
+ # @param colorize [Boolean] false to print the title plain whatever the color, for output that is not a terminal
19
+ # @raise [ArgumentError] when the color is neither
20
+ def initialize(results, title:, prefix:, color: 93, colorize: true)
21
+ unless color.nil? || COLORS.key?(color) || (color.is_a?(Integer) && color.between?(0, 255))
22
+ raise ArgumentError, "unknown color #{color.inspect}, use #{COLORS.keys.join(', ')} or a number from 0 to 255"
23
+ end
24
+
25
+ @results = results
26
+ @title = title
27
+ @prefix = prefix
28
+ @color = color
29
+ @colorize = colorize
30
+ end
31
+
32
+ # @return [String] the labels and their results, without the title
33
+ def summary
34
+ @results.map { |label, result| "#{label}: #{result.text}" }.join(' | ')
35
+ end
36
+
37
+ # @return [Symbol] the worst severity of any result
38
+ def severity
39
+ @results.values.map(&:severity).max_by { |severity| SEVERITIES.index(severity) } || :success
40
+ end
41
+
42
+ # @return [String]
43
+ def to_s
44
+ "\n#{@prefix}#{colored_title} #{summary}"
45
+ end
46
+
47
+ private
48
+
49
+ def colored_title
50
+ return "#{@title}:" unless @color && @colorize
51
+
52
+ "\e[38;5;#{COLORS.fetch(@color, @color)}m#{@title}:\e[0m"
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'guard/compat/plugin'
4
+
5
+ module Guard
6
+ class Status < Plugin
7
+ # What one segment shows, and how bad it is: a severity (`:success`, `:pending` or `:failed`, Guard's own
8
+ # notification types) and the text to display.
9
+ Result = Data.define(:severity, :text) do
10
+ def self.success = new(:success, '✅')
11
+
12
+ # Nothing recorded yet, for example before the first run.
13
+ def self.pending = new(:pending, '…')
14
+
15
+ # Something was recorded, but it could not be understood.
16
+ def self.unknown(detail = nil) = new(:failed, ['?', detail].compact.join(' '))
17
+ end
18
+
19
+ SEVERITIES = %i[success pending failed].freeze
20
+ end
21
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'guard/compat/plugin'
4
+ require_relative 'json_reader'
5
+ require_relative 'state_file'
6
+
7
+ module Guard
8
+ class Status < Plugin
9
+ # One labelled part of the line, and where its result comes from.
10
+ class Segment
11
+ SOURCES = { rubocop: RubocopReader, rspec: RspecReader, file: StateFile }.freeze
12
+
13
+ attr_reader :label
14
+
15
+ # @param label [String] shown before the result
16
+ # @param config [String, Hash] a path to a status file, or a Hash with exactly one of `rubocop:`, `rspec:`
17
+ # or `file:` set to a path, and for `file:` optionally `states:`
18
+ # @raise [ArgumentError] when the configuration cannot be made sense of
19
+ # @return [Segment]
20
+ def self.build(label, config)
21
+ config = { file: config } if config.is_a?(String)
22
+ raise ArgumentError, "segment #{label.inspect} must be a path or a Hash" unless config.is_a?(Hash)
23
+
24
+ config = config.transform_keys(&:to_sym)
25
+ source = sources(label, config)
26
+ new(label, reader(source, config))
27
+ end
28
+
29
+ def self.sources(label, config)
30
+ sources = SOURCES.keys & config.keys
31
+ raise ArgumentError, "segment #{label.inspect} needs exactly one of #{SOURCES.keys.inspect}" unless sources.one?
32
+
33
+ extra = config.keys - sources - (sources == [:file] ? [:states] : [])
34
+ raise ArgumentError, "segment #{label.inspect} has unknown keys #{extra.inspect}" unless extra.empty?
35
+
36
+ sources.first
37
+ end
38
+ private_class_method :sources
39
+
40
+ def self.reader(source, config)
41
+ source == :file ? StateFile.new(config[:file], config.fetch(:states, {})) : SOURCES[source].new(config[source])
42
+ end
43
+ private_class_method :reader
44
+
45
+ def initialize(label, reader)
46
+ @label = label.to_s
47
+ @reader = reader
48
+ end
49
+
50
+ # @return [Result]
51
+ def result = @reader.result
52
+
53
+ # @return [String] the file this segment reads
54
+ def path = @reader.path
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'guard/compat/plugin'
4
+ require_relative 'result'
5
+
6
+ module Guard
7
+ class Status < Plugin
8
+ # Reads a status file: the first word is the state, the rest are its arguments.
9
+ #
10
+ # ok
11
+ # slow 0.47 0.32
12
+ # error something went wrong
13
+ #
14
+ # `ok` and `error` are built in. Other states are declared with a display, in which `%{1}`, `%{2}` and so on
15
+ # stand for the arguments, counting from 1, and a severity. A state that is not declared shows as `? <state>` and
16
+ # counts as failed. A file that cannot be read shows as `?`, and control characters in it, such as terminal escape
17
+ # sequences, are treated as spaces.
18
+ class StateFile
19
+ BUILT_IN = {
20
+ 'ok' => { display: '✅', severity: :success },
21
+ 'error' => { display: '❌', severity: :failed }
22
+ }.freeze
23
+ ARGUMENT = /%\{(\d+)\}/
24
+
25
+ attr_reader :path
26
+
27
+ # @param path [String] the status file
28
+ # @param states [Hash{Symbol, String => Hash}] states to add to, or override, the built-in ones
29
+ # @raise [ArgumentError] when a state lacks a display, numbers its arguments from 0, or has a severity that is
30
+ # not Guard's
31
+ def initialize(path, states = {})
32
+ @path = path
33
+ @states = BUILT_IN.merge(states.to_h { |name, definition| [name.to_s, normalize(name, definition)] })
34
+ end
35
+
36
+ # @return [Result]
37
+ def result
38
+ state, *arguments = File.read(path).scrub.gsub(/[[:cntrl:]]/, ' ').split
39
+ return Result.pending unless state
40
+
41
+ definition = @states[state] or return Result.unknown(state)
42
+ Result.new(definition[:severity], fill(definition[:display], arguments))
43
+ rescue Errno::ENOENT
44
+ Result.pending
45
+ rescue SystemCallError
46
+ Result.unknown
47
+ end
48
+
49
+ private
50
+
51
+ def fill(display, arguments)
52
+ display.gsub(ARGUMENT) { arguments[Regexp.last_match(1).to_i - 1] }
53
+ end
54
+
55
+ def normalize(name, definition)
56
+ definition = definition.to_h.transform_keys(&:to_sym)
57
+ raise ArgumentError, "state #{name.inspect} needs a display, a String" unless definition[:display].is_a?(String)
58
+ unless SEVERITIES.include?(definition[:severity])
59
+ raise ArgumentError, "state #{name.inspect} needs a severity, one of #{SEVERITIES.inspect}"
60
+ end
61
+
62
+ check_arguments(name, definition[:display])
63
+
64
+ definition
65
+ end
66
+
67
+ def check_arguments(name, display)
68
+ return unless display.scan(ARGUMENT).any? { |(number)| number.to_i < 1 }
69
+
70
+ raise ArgumentError, "state #{name.inspect} counts its arguments from 1"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,4 @@
1
+ guard :status, segments: {
2
+ 'RuboCop' => { rubocop: 'tmp/rubocop.json' },
3
+ 'Tests' => { rspec: 'tmp/rspec.json' }
4
+ }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Guard
4
+ module StatusVersion
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'guard/compat/plugin'
4
+ require_relative 'status/version'
5
+ require_relative 'status/line'
6
+ require_relative 'status/segment'
7
+
8
+ module Guard
9
+ # Prints one line summarizing what the other guards recorded.
10
+ #
11
+ # Guard runs plugins in the order of the Guardfile, so this one goes last. It prints after a change only when
12
+ # another guard started a run for it, which it learns from the callbacks Guard fires before each task.
13
+ #
14
+ # @example Guardfile
15
+ # guard :status, segments: {
16
+ # 'RuboCop' => { rubocop: 'tmp/rubocop.json' },
17
+ # 'Tests' => { rspec: 'tmp/rspec.json' },
18
+ # 'Docs' => 'tmp/docs_status.txt',
19
+ # 'Bench' => { file: 'tmp/benchmark_status.txt',
20
+ # states: { slow: { display: '🐢 (%{1}s vs %{2}s)', severity: :failed } } }
21
+ # }
22
+ class Status < Plugin
23
+ DEFAULT_SEGMENTS = {
24
+ 'RuboCop' => { rubocop: 'tmp/rubocop.json' },
25
+ 'Tests' => { rspec: 'tmp/rspec.json' }
26
+ }.freeze
27
+ # The tasks that run in response to changes. `_begin` fires even when the run goes on to fail, unlike `_end`.
28
+ RUN_EVENTS = %i[run_all_begin run_on_modifications_begin run_on_additions_begin run_on_removals_begin].freeze
29
+
30
+ # @param options [Hash]
31
+ # @option options [Hash{String => String, Hash}] :segments the labels and where their results come from, in the
32
+ # order to show them
33
+ # @option options [String] :title ('Status')
34
+ # @option options [String] :prefix ('🔮') shown before the title
35
+ # @option options [Symbol, Integer, nil] :title_color (93) a color name from {Line::COLORS}, a 256-color
36
+ # number from 0 to 255, or nil for none
37
+ # @option options [Boolean] :notify (false) also send the summary as a Guard notification
38
+ # @raise [ArgumentError] when a segment cannot be made sense of
39
+ def initialize(options = {})
40
+ super
41
+ @segments = options.fetch(:segments, DEFAULT_SEGMENTS).map { |label, config| Segment.build(label, config) }
42
+ self.watchers = [Guard::Watcher.new(/./)] if watchers.empty?
43
+ @title = options.fetch(:title, 'Status')
44
+ @others_ran = false
45
+ @followed = {}.compare_by_identity
46
+ build_line({}) # fail on a bad color when Guard starts
47
+ end
48
+
49
+ # @return [void]
50
+ def start
51
+ follow_others
52
+ print_status
53
+ end
54
+
55
+ # @return [void]
56
+ def reload
57
+ follow_others
58
+ print_status
59
+ end
60
+
61
+ # @return [void]
62
+ def run_all
63
+ @others_ran = false
64
+ print_status
65
+ end
66
+
67
+ # Prints if another guard has run since the last time, whatever changed.
68
+ #
69
+ # @return [void]
70
+ def run_on_modifications(_paths = nil)
71
+ return unless @others_ran
72
+
73
+ @others_ran = false
74
+ print_status
75
+ end
76
+ alias run_on_additions run_on_modifications
77
+ alias run_on_removals run_on_modifications
78
+
79
+ private
80
+
81
+ # Listens for the other plugins starting a run. Guard reuses the plugins on reload, but this is repeated in case
82
+ # the list has changed, and @followed gives each plugin one callback.
83
+ def follow_others
84
+ Guard.state.session.plugins.all.each do |plugin|
85
+ next if plugin.equal?(self) || @followed.key?(plugin)
86
+
87
+ @followed[plugin] = true
88
+ # Callbacks are kept per plugin class, and that is where Guard looks when it fires them
89
+ plugin.class.add_callback(method(:other_ran), plugin, RUN_EVENTS)
90
+ end
91
+ end
92
+
93
+ def other_ran(*)
94
+ @others_ran = true
95
+ end
96
+
97
+ def print_status
98
+ line = build_line(@segments.to_h { |segment| [segment.label, segment.result] })
99
+ $stdout.puts line
100
+ Compat::UI.notify(line.summary, title: @title, image: line.severity) if options[:notify]
101
+ end
102
+
103
+ def build_line(results)
104
+ Line.new(results, title: @title, prefix: options.fetch(:prefix, '🔮'),
105
+ color: options.fetch(:title_color, 93), colorize: color?)
106
+ end
107
+
108
+ # Colors only a terminal, and not when NO_COLOR asks for none (https://no-color.org).
109
+ def color?
110
+ $stdout.tty? && ENV.fetch('NO_COLOR', '').empty?
111
+ end
112
+ end
113
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: guard-status
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Joe Sharp
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: guard
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.20'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '2.20'
26
+ - !ruby/object:Gem::Dependency
27
+ name: guard-compat
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '1.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.2'
40
+ description: guard-status reads what your other Guard plugins recorded, such as RuboCop
41
+ and RSpec JSON reports or small status files, and prints a single line summarizing
42
+ them after every run.
43
+ email:
44
+ - joesharp13@gmail.com
45
+ executables:
46
+ - guard-status-colors
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - CHANGELOG.md
51
+ - CODE_OF_CONDUCT.md
52
+ - LICENSE.txt
53
+ - README.md
54
+ - bin/guard-status-colors
55
+ - lib/guard/status.rb
56
+ - lib/guard/status/json_reader.rb
57
+ - lib/guard/status/line.rb
58
+ - lib/guard/status/result.rb
59
+ - lib/guard/status/segment.rb
60
+ - lib/guard/status/state_file.rb
61
+ - lib/guard/status/templates/Guardfile
62
+ - lib/guard/status/version.rb
63
+ homepage: https://github.com/joe-sharp/guard-status
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ homepage_uri: https://github.com/joe-sharp/guard-status
68
+ source_code_uri: https://github.com/joe-sharp/guard-status/tree/v0.1.0
69
+ bug_tracker_uri: https://github.com/joe-sharp/guard-status/issues
70
+ changelog_uri: https://github.com/joe-sharp/guard-status/blob/main/CHANGELOG.md
71
+ rubygems_mfa_required: 'true'
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 3.3.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 4.0.20
87
+ specification_version: 4
88
+ summary: Guard plugin that prints one status line for all your other guards
89
+ test_files: []