rbwatch 1.0.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/README.md +217 -0
- data/bin/rbw +7 -0
- data/bin/rbwatch +7 -0
- data/exe/rbw +5 -0
- data/exe/rbwatch +5 -0
- data/lib/rbwatch/cli.rb +135 -0
- data/lib/rbwatch/configuration.rb +299 -0
- data/lib/rbwatch/dashboard.rb +244 -0
- data/lib/rbwatch/file_scanner.rb +101 -0
- data/lib/rbwatch/process_manager.rb +142 -0
- data/lib/rbwatch/runner.rb +239 -0
- data/lib/rbwatch/theme.rb +226 -0
- data/lib/rbwatch/version.rb +5 -0
- data/lib/rbwatch/watcher.rb +98 -0
- data/lib/rbwatch.rb +15 -0
- metadata +184 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9ff7a8e55dbe3107b8476b224e28e0da6a2d13caceded52652a23449c95878db
|
|
4
|
+
data.tar.gz: 5f956a57c909f30f0625dd80e4765988b11abb3377b6d15526daf51662356840
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 153366c2a7cda009abec391429663ceddd20745003198c063f354189189a5f7ca794a6bde5285994bbd095abb53853796f24dc7c1c86d44c234138faf8b946fe
|
|
7
|
+
data.tar.gz: 78d5d5eb3edf0d759b840b228795cb6134ab8f03bd9223ecd2c224a98d701fbb9a9dc0730cc7a24b65c92e1d1a4b8d99eeb7cc47a54e0f07794dc027fcb40640
|
data/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# RBWatch
|
|
2
|
+
|
|
3
|
+
RBWatch is a Ruby process watcher inspired by nodemon.
|
|
4
|
+
|
|
5
|
+
It watches source files recursively and restarts the process automatically when changes are detected.
|
|
6
|
+
|
|
7
|
+
## Goals
|
|
8
|
+
|
|
9
|
+
- Clean architecture
|
|
10
|
+
- Delightful terminal output
|
|
11
|
+
- Reliable process lifecycle management
|
|
12
|
+
- Works on Linux, macOS, and WSL
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bundle install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If you install RBWatch globally with RubyGems, the `rbwatch` and `rbw` executables are available directly on your shell `PATH`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
gem install rbwatch
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
If you want to use RBWatch from another project while developing locally, add it to that project's `Gemfile`:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
gem "rbwatch", path: "../../../open-source/rbwatch"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then run:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bundle install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### From this repository checkout
|
|
41
|
+
|
|
42
|
+
Run a Ruby app directly:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
rbwatch app.rb
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If you are working from this repository checkout:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
bundle exec exe/rbwatch app.rb
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Or use the alias:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
rbw app.rb
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
If you are running from this repository checkout, use:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
bundle exec exe/rbw app.rb
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### From another project
|
|
67
|
+
|
|
68
|
+
If RBWatch is added to another project's `Gemfile`, run it through Bundler:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bundle exec rbwatch app.rb
|
|
72
|
+
bundle exec rbw app.rb
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Bundler makes the executables available to the bundle, but it does not place them on your shell `PATH`.
|
|
76
|
+
|
|
77
|
+
If you want a direct command without `bundle exec`, generate binstubs in that project:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
bundle binstubs rbwatch
|
|
81
|
+
bin/rbw app.rb
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If the first argument is a Ruby file, RBWatch expands it to `ruby app.rb` automatically.
|
|
85
|
+
|
|
86
|
+
You can still run explicit commands:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
bundle exec exe/rbwatch ruby app.rb
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Or watch a Bundler-driven command:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
bundle exec exe/rbwatch bundle exec ruby app.rb
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Useful flags:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
bundle exec exe/rbwatch --delay 500 ruby app.rb
|
|
102
|
+
bundle exec exe/rbwatch --ext rb,erb,yml ruby app.rb
|
|
103
|
+
bundle exec exe/rbwatch --ignore tmp,log,node_modules ruby app.rb
|
|
104
|
+
bundle exec exe/rbwatch --clear ruby app.rb
|
|
105
|
+
bundle exec exe/rbwatch --no-clear ruby app.rb
|
|
106
|
+
bundle exec exe/rbwatch --verbose ruby app.rb
|
|
107
|
+
bundle exec exe/rbwatch --no-banner ruby app.rb
|
|
108
|
+
bundle exec exe/rbwatch --no-color ruby app.rb
|
|
109
|
+
bundle exec exe/rbwatch --theme neon ruby app.rb
|
|
110
|
+
bundle exec exe/rbwatch --watch app,config ruby app.rb
|
|
111
|
+
bundle exec exe/rbwatch --config rbwatch.yml ruby app.rb
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Configuration File
|
|
115
|
+
|
|
116
|
+
RBWatch can load a YAML configuration file with `--config` or automatically discover `rbwatch.yml`, `rbwatch.yaml`, `.rbwatch.yml`, or `.rbwatch.yaml` in the current directory.
|
|
117
|
+
|
|
118
|
+
Example `rbwatch.yml`:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
delay: 300
|
|
122
|
+
extensions:
|
|
123
|
+
- rb
|
|
124
|
+
- erb
|
|
125
|
+
- yml
|
|
126
|
+
ignore:
|
|
127
|
+
- tmp
|
|
128
|
+
- log
|
|
129
|
+
watch_paths:
|
|
130
|
+
- app
|
|
131
|
+
- config
|
|
132
|
+
command: ruby app.rb
|
|
133
|
+
clear: true
|
|
134
|
+
verbose: true
|
|
135
|
+
banner: true
|
|
136
|
+
color: true
|
|
137
|
+
theme: neon
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
CLI options always win over values from the config file.
|
|
141
|
+
|
|
142
|
+
## Defaults
|
|
143
|
+
|
|
144
|
+
RBWatch watches these extensions by default:
|
|
145
|
+
|
|
146
|
+
- `rb`
|
|
147
|
+
- `erb`
|
|
148
|
+
- `haml`
|
|
149
|
+
- `slim`
|
|
150
|
+
- `rake`
|
|
151
|
+
- `yml`
|
|
152
|
+
- `yaml`
|
|
153
|
+
- `json`
|
|
154
|
+
|
|
155
|
+
Ignored paths by default:
|
|
156
|
+
|
|
157
|
+
- `log`
|
|
158
|
+
- `tmp`
|
|
159
|
+
- `.git`
|
|
160
|
+
- `node_modules`
|
|
161
|
+
|
|
162
|
+
Default debounce delay: `300ms`
|
|
163
|
+
|
|
164
|
+
By default, RBWatch clears the visible terminal and scrollback before each restart when it is attached to a TTY. Use `--no-clear` or `clear: false` if you want to keep previous output on screen.
|
|
165
|
+
|
|
166
|
+
The startup dashboard shows the command, watched roots, theme, and config file when available.
|
|
167
|
+
|
|
168
|
+
If you pass `--stats`, the status card also shows restart count and uptime.
|
|
169
|
+
|
|
170
|
+
## Architecture
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
rbwatch/
|
|
174
|
+
├── bin/
|
|
175
|
+
│ ├── rbwatch
|
|
176
|
+
│ └── rbw
|
|
177
|
+
├── exe/
|
|
178
|
+
│ ├── rbwatch
|
|
179
|
+
│ └── rbw
|
|
180
|
+
├── lib/
|
|
181
|
+
│ └── rbwatch/
|
|
182
|
+
│ ├── cli.rb
|
|
183
|
+
│ ├── configuration.rb
|
|
184
|
+
│ ├── dashboard.rb
|
|
185
|
+
│ ├── file_scanner.rb
|
|
186
|
+
│ ├── process_manager.rb
|
|
187
|
+
│ ├── runner.rb
|
|
188
|
+
│ ├── theme.rb
|
|
189
|
+
│ ├── watcher.rb
|
|
190
|
+
│ ├── version.rb
|
|
191
|
+
│ └── rbwatch.rb
|
|
192
|
+
└── spec/
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Notes
|
|
196
|
+
|
|
197
|
+
- Process spawning uses `spawn`.
|
|
198
|
+
- Restarting uses `Process.kill("TERM", pid)` first and escalates if the process refuses to exit.
|
|
199
|
+
- CTRL+C and `TERM` are handled gracefully.
|
|
200
|
+
- The UI uses TTY gems for boxed layouts, cursors, spinners, tables, and screen sizing.
|
|
201
|
+
- Fast startup failures do not restart in a tight loop; RBWatch waits for file changes before trying again.
|
|
202
|
+
|
|
203
|
+
## Future-ready hooks
|
|
204
|
+
|
|
205
|
+
The configuration layer already leaves room for:
|
|
206
|
+
|
|
207
|
+
- `--exec`
|
|
208
|
+
- `--config rbwatch.yml`
|
|
209
|
+
- `--watch app,config`
|
|
210
|
+
- `--sound`
|
|
211
|
+
- `--desktop-notify`
|
|
212
|
+
- `--history`
|
|
213
|
+
- `--theme neon`
|
|
214
|
+
- `--theme minimal`
|
|
215
|
+
- `--theme retro`
|
|
216
|
+
- `--theme ruby`
|
|
217
|
+
- `--theme matrix`
|
data/bin/rbw
ADDED
data/bin/rbwatch
ADDED
data/exe/rbw
ADDED
data/exe/rbwatch
ADDED
data/lib/rbwatch/cli.rb
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
|
|
5
|
+
module RBWatch
|
|
6
|
+
class CLI
|
|
7
|
+
def self.start(argv = ARGV, stdout: $stdout, stderr: $stderr, stdin: $stdin)
|
|
8
|
+
new(argv, stdout: stdout, stderr: stderr, stdin: stdin).run
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(argv, stdout:, stderr:, stdin:)
|
|
12
|
+
@argv = argv.dup
|
|
13
|
+
@stdout = stdout
|
|
14
|
+
@stderr = stderr
|
|
15
|
+
@stdin = stdin
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
configuration = parse
|
|
20
|
+
if configuration == :help
|
|
21
|
+
@stdout.puts parser if @show_help
|
|
22
|
+
return 0
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Runner.new(configuration: configuration).run
|
|
26
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
27
|
+
@stderr.puts e.message
|
|
28
|
+
@stderr.puts
|
|
29
|
+
@stderr.puts parser
|
|
30
|
+
1
|
|
31
|
+
rescue Theme::UnknownThemeError => e
|
|
32
|
+
@stderr.puts e.message
|
|
33
|
+
1
|
|
34
|
+
rescue ArgumentError => e
|
|
35
|
+
@stderr.puts e.message
|
|
36
|
+
@stderr.puts
|
|
37
|
+
@stderr.puts parser
|
|
38
|
+
1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse
|
|
42
|
+
options = default_options
|
|
43
|
+
parser = build_parser(options)
|
|
44
|
+
parser.parse!(@argv)
|
|
45
|
+
|
|
46
|
+
if options.delete(:help_requested)
|
|
47
|
+
@show_help = true
|
|
48
|
+
return :help
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
configuration = Configuration.from_sources(
|
|
52
|
+
cli_options: options,
|
|
53
|
+
command: @argv,
|
|
54
|
+
stdin: @stdin,
|
|
55
|
+
stdout: @stdout,
|
|
56
|
+
stderr: @stderr
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if configuration.command.empty?
|
|
60
|
+
@stderr.puts parser
|
|
61
|
+
@show_help = false
|
|
62
|
+
return :help
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
configuration
|
|
66
|
+
ensure
|
|
67
|
+
@parser = parser
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def parser
|
|
71
|
+
@parser || build_parser(default_options)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def default_options
|
|
77
|
+
{
|
|
78
|
+
delay: nil,
|
|
79
|
+
extensions: nil,
|
|
80
|
+
ignore: nil,
|
|
81
|
+
watch_paths: nil,
|
|
82
|
+
clear: nil,
|
|
83
|
+
verbose: nil,
|
|
84
|
+
banner: nil,
|
|
85
|
+
color: nil,
|
|
86
|
+
theme_name: nil,
|
|
87
|
+
restart_on_exit: nil,
|
|
88
|
+
sound: nil,
|
|
89
|
+
desktop_notify: nil,
|
|
90
|
+
stats: nil,
|
|
91
|
+
history: nil,
|
|
92
|
+
config_file: nil,
|
|
93
|
+
exec_mode: nil,
|
|
94
|
+
help_requested: false
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def build_parser(options)
|
|
99
|
+
OptionParser.new do |opts|
|
|
100
|
+
opts.banner = "Usage: rbwatch [options] command [args...]"
|
|
101
|
+
opts.separator ""
|
|
102
|
+
opts.separator "Examples:"
|
|
103
|
+
opts.separator " rbwatch app.rb"
|
|
104
|
+
opts.separator " rbwatch ruby app.rb"
|
|
105
|
+
opts.separator " rbw app.rb"
|
|
106
|
+
opts.separator " rbwatch bundle exec ruby app.rb"
|
|
107
|
+
opts.separator " rbwatch --config rbwatch.yml ruby app.rb"
|
|
108
|
+
opts.separator ""
|
|
109
|
+
opts.separator "Options:"
|
|
110
|
+
|
|
111
|
+
opts.on("--delay MS", Integer, "Debounce delay in milliseconds (default 300)") { |value| options[:delay] = value }
|
|
112
|
+
opts.on("--ext EXTENSIONS", String, "Comma-separated extensions") { |value| options[:extensions] = split_csv(value) }
|
|
113
|
+
opts.on("--ignore PATHS", String, "Comma-separated ignored paths") { |value| options[:ignore] = split_csv(value) }
|
|
114
|
+
opts.on("--watch PATHS", String, "Comma-separated watch roots") { |value| options[:watch_paths] = split_csv(value) }
|
|
115
|
+
opts.on("--theme NAME", String, "Theme name: ruby, neon, minimal, matrix") { |value| options[:theme_name] = value }
|
|
116
|
+
opts.on("--clear", "Clear the terminal and scrollback before each restart") { options[:clear] = true }
|
|
117
|
+
opts.on("--no-clear", "Keep previous terminal output between restarts") { options[:clear] = false }
|
|
118
|
+
opts.on("--verbose", "Show additional logs") { options[:verbose] = true }
|
|
119
|
+
opts.on("--no-banner", "Hide the startup banner") { options[:banner] = false }
|
|
120
|
+
opts.on("--no-color", "Disable ANSI colors") { options[:color] = false }
|
|
121
|
+
opts.on("--sound", "Reserved for future sound notifications") { options[:sound] = true }
|
|
122
|
+
opts.on("--desktop-notify", "Reserved for desktop notifications") { options[:desktop_notify] = true }
|
|
123
|
+
opts.on("--stats", "Show restart count and uptime in the status card") { options[:stats] = true }
|
|
124
|
+
opts.on("--history", "Reserved for restart history") { options[:history] = true }
|
|
125
|
+
opts.on("--config PATH", String, "Load settings from a YAML configuration file") { |value| options[:config_file] = value }
|
|
126
|
+
opts.on("--exec", "Reserved for exec-mode launches") { options[:exec_mode] = true }
|
|
127
|
+
opts.on_tail("-h", "--help", "Show this help") { options[:help_requested] = true }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def split_csv(value)
|
|
132
|
+
value.to_s.split(",").map(&:strip).reject(&:empty?)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module RBWatch
|
|
7
|
+
class Configuration
|
|
8
|
+
DEFAULT_DELAY = 300
|
|
9
|
+
DEFAULT_EXTENSIONS = %w[rb erb haml slim rake yml yaml json].freeze
|
|
10
|
+
DEFAULT_IGNORE = %w[log tmp .git node_modules].freeze
|
|
11
|
+
DEFAULT_WATCH_PATHS = ["."].freeze
|
|
12
|
+
CONFIG_FILENAMES = %w[rbwatch.yml rbwatch.yaml .rbwatch.yml .rbwatch.yaml].freeze
|
|
13
|
+
SHORTHAND_RUBY_EXTENSIONS = %w[.rb .ru].freeze
|
|
14
|
+
|
|
15
|
+
attr_reader :delay,
|
|
16
|
+
:extensions,
|
|
17
|
+
:ignore,
|
|
18
|
+
:watch_paths,
|
|
19
|
+
:command,
|
|
20
|
+
:clear,
|
|
21
|
+
:verbose,
|
|
22
|
+
:banner,
|
|
23
|
+
:color,
|
|
24
|
+
:theme_name,
|
|
25
|
+
:restart_on_exit,
|
|
26
|
+
:sound,
|
|
27
|
+
:desktop_notify,
|
|
28
|
+
:stats,
|
|
29
|
+
:history,
|
|
30
|
+
:config_file,
|
|
31
|
+
:exec_mode,
|
|
32
|
+
:stdin,
|
|
33
|
+
:stdout,
|
|
34
|
+
:stderr
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def discover_config_file(cwd = Dir.pwd)
|
|
38
|
+
CONFIG_FILENAMES.map { |name| File.join(cwd, name) }.find { |path| File.file?(path) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def load_file(path)
|
|
42
|
+
return {} if path.nil? || path.to_s.strip.empty?
|
|
43
|
+
|
|
44
|
+
raw = YAML.safe_load(File.read(path), permitted_classes: [Symbol], aliases: true)
|
|
45
|
+
raise ArgumentError, "Config file must contain a YAML mapping" unless raw.nil? || raw.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
normalize_config_hash(raw || {})
|
|
48
|
+
rescue Psych::SyntaxError => e
|
|
49
|
+
raise ArgumentError, "Unable to parse config file #{path}: #{e.message}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def from_sources(cli_options:, command:, stdin:, stdout:, stderr:)
|
|
53
|
+
cli_options = symbolize_keys(cli_options)
|
|
54
|
+
explicit_config_file = cli_options.delete(:config_file)
|
|
55
|
+
config_path = explicit_config_file || discover_config_file
|
|
56
|
+
file_options = config_path ? load_file(config_path) : {}
|
|
57
|
+
|
|
58
|
+
merged = default_options.merge(file_options).merge(cli_options.compact)
|
|
59
|
+
resolved_command = command.empty? ? Array(merged[:command]) : command
|
|
60
|
+
|
|
61
|
+
new(
|
|
62
|
+
**merged.merge(command: resolved_command, config_file: config_path),
|
|
63
|
+
stdin: stdin,
|
|
64
|
+
stdout: stdout,
|
|
65
|
+
stderr: stderr
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def default_options
|
|
70
|
+
{
|
|
71
|
+
delay: DEFAULT_DELAY,
|
|
72
|
+
extensions: DEFAULT_EXTENSIONS.dup,
|
|
73
|
+
ignore: DEFAULT_IGNORE.dup,
|
|
74
|
+
watch_paths: DEFAULT_WATCH_PATHS.dup,
|
|
75
|
+
command: [],
|
|
76
|
+
clear: nil,
|
|
77
|
+
verbose: false,
|
|
78
|
+
banner: true,
|
|
79
|
+
color: true,
|
|
80
|
+
theme_name: :ruby,
|
|
81
|
+
restart_on_exit: true,
|
|
82
|
+
sound: false,
|
|
83
|
+
desktop_notify: false,
|
|
84
|
+
stats: false,
|
|
85
|
+
history: false,
|
|
86
|
+
config_file: nil,
|
|
87
|
+
exec_mode: false
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def normalize_config_hash(hash)
|
|
92
|
+
hash.each_with_object({}) do |(key, value), options|
|
|
93
|
+
case key.to_s
|
|
94
|
+
when "delay"
|
|
95
|
+
options[:delay] = value
|
|
96
|
+
when "extensions"
|
|
97
|
+
options[:extensions] = normalize_list(value)
|
|
98
|
+
when "ignore"
|
|
99
|
+
options[:ignore] = normalize_list(value)
|
|
100
|
+
when "watch_paths", "watch"
|
|
101
|
+
options[:watch_paths] = normalize_watch_paths(value)
|
|
102
|
+
when "command"
|
|
103
|
+
options[:command] = normalize_command(value)
|
|
104
|
+
when "clear", "verbose", "banner", "color", "restart_on_exit", "sound", "desktop_notify", "stats", "history", "exec_mode"
|
|
105
|
+
options[key.to_sym] = !!value
|
|
106
|
+
when "theme", "theme_name"
|
|
107
|
+
options[:theme_name] = Theme.normalize_name(value) || :ruby
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def normalize_list(value)
|
|
113
|
+
Array(value).flat_map { |item| item.to_s.split(",") }
|
|
114
|
+
.map { |entry| entry.strip.downcase }
|
|
115
|
+
.reject(&:empty?)
|
|
116
|
+
.uniq
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def normalize_watch_paths(value)
|
|
120
|
+
paths = Array(value).flat_map { |item| item.to_s.split(",") }
|
|
121
|
+
normalized = paths.map { |path| path.strip }.reject(&:empty?)
|
|
122
|
+
normalized = ["."] if normalized.empty?
|
|
123
|
+
normalized.uniq
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def normalize_command(value)
|
|
127
|
+
command = case value
|
|
128
|
+
when nil
|
|
129
|
+
[]
|
|
130
|
+
when String
|
|
131
|
+
Shellwords.split(value)
|
|
132
|
+
else
|
|
133
|
+
Array(value).flat_map do |entry|
|
|
134
|
+
entry.is_a?(String) ? Shellwords.split(entry) : entry
|
|
135
|
+
end.map(&:to_s)
|
|
136
|
+
end
|
|
137
|
+
expand_command_shorthand(command)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def expand_command_shorthand(command)
|
|
141
|
+
return command if command.length != 1
|
|
142
|
+
|
|
143
|
+
first = command.first.to_s
|
|
144
|
+
return command unless ruby_script_path?(first)
|
|
145
|
+
|
|
146
|
+
["ruby", first]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def ruby_script_path?(path)
|
|
150
|
+
SHORTHAND_RUBY_EXTENSIONS.include?(File.extname(path).downcase)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def symbolize_keys(hash)
|
|
154
|
+
hash.each_with_object({}) { |(key, value), result| result[key.to_sym] = value }
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def initialize(
|
|
159
|
+
delay: DEFAULT_DELAY,
|
|
160
|
+
extensions: DEFAULT_EXTENSIONS,
|
|
161
|
+
ignore: DEFAULT_IGNORE,
|
|
162
|
+
watch_paths: DEFAULT_WATCH_PATHS,
|
|
163
|
+
command: [],
|
|
164
|
+
clear: nil,
|
|
165
|
+
verbose: false,
|
|
166
|
+
banner: true,
|
|
167
|
+
color: true,
|
|
168
|
+
theme_name: "ruby",
|
|
169
|
+
restart_on_exit: true,
|
|
170
|
+
sound: false,
|
|
171
|
+
desktop_notify: false,
|
|
172
|
+
stats: false,
|
|
173
|
+
history: false,
|
|
174
|
+
config_file: nil,
|
|
175
|
+
exec_mode: false,
|
|
176
|
+
stdin: $stdin,
|
|
177
|
+
stdout: $stdout,
|
|
178
|
+
stderr: $stderr
|
|
179
|
+
)
|
|
180
|
+
@delay = Integer(delay)
|
|
181
|
+
@extensions = normalize_list(extensions)
|
|
182
|
+
@ignore = normalize_list(ignore)
|
|
183
|
+
@watch_paths = normalize_watch_paths(watch_paths)
|
|
184
|
+
@command = normalize_command(command)
|
|
185
|
+
@verbose = !!verbose
|
|
186
|
+
@banner = !!banner
|
|
187
|
+
@color = !!color
|
|
188
|
+
@theme_name = Theme.normalize_name(theme_name) || :ruby
|
|
189
|
+
@restart_on_exit = !!restart_on_exit
|
|
190
|
+
@sound = !!sound
|
|
191
|
+
@desktop_notify = !!desktop_notify
|
|
192
|
+
@stats = !!stats
|
|
193
|
+
@history = !!history
|
|
194
|
+
@config_file = config_file
|
|
195
|
+
@exec_mode = !!exec_mode
|
|
196
|
+
@stdin = stdin
|
|
197
|
+
@stdout = stdout
|
|
198
|
+
@stderr = stderr
|
|
199
|
+
@clear = clear.nil? ? clear_by_default?(stderr) : !!clear
|
|
200
|
+
@theme = nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def color?
|
|
204
|
+
@color
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def verbose?
|
|
208
|
+
@verbose
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def clear?
|
|
212
|
+
@clear
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def banner?
|
|
216
|
+
@banner
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def restart_on_exit?
|
|
220
|
+
@restart_on_exit
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def sound?
|
|
224
|
+
@sound
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def desktop_notify?
|
|
228
|
+
@desktop_notify
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def stats?
|
|
232
|
+
@stats
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def history?
|
|
236
|
+
@history
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def exec_mode?
|
|
240
|
+
@exec_mode
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def theme
|
|
244
|
+
@theme ||= Theme.resolve(theme_name, color: color?, interactive: interactive_theme_prompt?, input: stdin, output: stderr)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def delay_seconds
|
|
248
|
+
delay / 1000.0
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def command_string
|
|
252
|
+
Shellwords.join(command)
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def interactive_theme_prompt?
|
|
256
|
+
stdin.respond_to?(:tty?) && stdout.respond_to?(:tty?) && stdin.tty? && stdout.tty?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def to_h
|
|
260
|
+
{
|
|
261
|
+
delay: delay,
|
|
262
|
+
extensions: extensions.dup,
|
|
263
|
+
ignore: ignore.dup,
|
|
264
|
+
watch_paths: watch_paths.dup,
|
|
265
|
+
command: command.dup,
|
|
266
|
+
clear: clear?,
|
|
267
|
+
verbose: verbose?,
|
|
268
|
+
banner: banner?,
|
|
269
|
+
color: color?,
|
|
270
|
+
theme_name: theme_name,
|
|
271
|
+
restart_on_exit: restart_on_exit?,
|
|
272
|
+
sound: sound?,
|
|
273
|
+
desktop_notify: desktop_notify?,
|
|
274
|
+
stats: stats?,
|
|
275
|
+
history: history?,
|
|
276
|
+
config_file: config_file,
|
|
277
|
+
exec_mode: exec_mode?
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private
|
|
282
|
+
|
|
283
|
+
def normalize_list(values)
|
|
284
|
+
self.class.normalize_list(values)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def normalize_watch_paths(values)
|
|
288
|
+
self.class.normalize_watch_paths(values)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def normalize_command(value)
|
|
292
|
+
self.class.normalize_command(value)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def clear_by_default?(stderr)
|
|
296
|
+
stderr.respond_to?(:tty?) && stderr.tty?
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|