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 +7 -0
- data/CHANGELOG.md +10 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +143 -0
- data/bin/guard-status-colors +9 -0
- data/lib/guard/status/json_reader.rb +62 -0
- data/lib/guard/status/line.rb +56 -0
- data/lib/guard/status/result.rb +21 -0
- data/lib/guard/status/segment.rb +57 -0
- data/lib/guard/status/state_file.rb +74 -0
- data/lib/guard/status/templates/Guardfile +4 -0
- data/lib/guard/status/version.rb +7 -0
- data/lib/guard/status.rb +113 -0
- metadata +89 -0
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.
|
data/CODE_OF_CONDUCT.md
ADDED
|
@@ -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
|
+

|
|
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
|
+

|
|
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
|
data/lib/guard/status.rb
ADDED
|
@@ -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: []
|