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.
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colorize"
4
+ require "pastel"
5
+ require "tty-prompt"
6
+ require "tty-screen"
7
+
8
+ module RBWatch
9
+ class Theme
10
+ Definition = Struct.new(
11
+ :banner_border,
12
+ :status_border,
13
+ :restart_border,
14
+ :border_color,
15
+ :accent_color,
16
+ :info_color,
17
+ :success_color,
18
+ :warning_color,
19
+ :error_color,
20
+ :restart_color,
21
+ :muted_color,
22
+ keyword_init: true
23
+ )
24
+
25
+ DEFINITIONS = {
26
+ ruby: Definition.new(
27
+ banner_border: :thick,
28
+ status_border: :light,
29
+ restart_border: :thick,
30
+ border_color: :red,
31
+ accent_color: :red,
32
+ info_color: :cyan,
33
+ success_color: :green,
34
+ warning_color: :yellow,
35
+ error_color: :red,
36
+ restart_color: :magenta,
37
+ muted_color: :bright_black
38
+ ),
39
+ neon: Definition.new(
40
+ banner_border: :thick,
41
+ status_border: :light,
42
+ restart_border: :thick,
43
+ border_color: :cyan,
44
+ accent_color: :magenta,
45
+ info_color: :cyan,
46
+ success_color: :green,
47
+ warning_color: :yellow,
48
+ error_color: :red,
49
+ restart_color: :magenta,
50
+ muted_color: :bright_black
51
+ ),
52
+ minimal: Definition.new(
53
+ banner_border: :ascii,
54
+ status_border: :ascii,
55
+ restart_border: :ascii,
56
+ border_color: :bright_black,
57
+ accent_color: :white,
58
+ info_color: :white,
59
+ success_color: :white,
60
+ warning_color: :white,
61
+ error_color: :white,
62
+ restart_color: :white,
63
+ muted_color: :bright_black
64
+ ),
65
+ matrix: Definition.new(
66
+ banner_border: :light,
67
+ status_border: :light,
68
+ restart_border: :thick,
69
+ border_color: :green,
70
+ accent_color: :green,
71
+ info_color: :green,
72
+ success_color: :green,
73
+ warning_color: :yellow,
74
+ error_color: :red,
75
+ restart_color: :green,
76
+ muted_color: :bright_black
77
+ )
78
+ }.freeze
79
+
80
+ class UnknownThemeError < ArgumentError; end
81
+
82
+ class << self
83
+ def available
84
+ DEFINITIONS.keys
85
+ end
86
+
87
+ def default(color: true)
88
+ new(:ruby, color: color)
89
+ end
90
+
91
+ def normalize_name(name)
92
+ return nil if name.nil?
93
+
94
+ value = name.to_s.strip.downcase
95
+ return nil if value.empty?
96
+
97
+ value.to_sym
98
+ end
99
+
100
+ def resolve(name, color: true, interactive: false, input: $stdin, output: $stderr)
101
+ key = normalize_name(name)
102
+ return default(color: color) if key.nil?
103
+ return new(key, color: color) if DEFINITIONS.key?(key)
104
+
105
+ if interactive && input.tty? && output.tty?
106
+ choice = TTY::Prompt.new.select("Choose a theme", available.map(&:to_s), default: "ruby")
107
+ return new(choice.to_sym, color: color)
108
+ end
109
+
110
+ raise UnknownThemeError, "Unknown theme `#{name}`. Available themes: #{available.map(&:to_s).join(', ')}"
111
+ end
112
+ end
113
+
114
+ attr_reader :name, :definition
115
+
116
+ def initialize(name = :ruby, color: true)
117
+ @name = self.class.normalize_name(name) || :ruby
118
+ @definition = self.class::DEFINITIONS.fetch(@name, self.class::DEFINITIONS.fetch(:ruby))
119
+ @color = color
120
+ @pastel = Pastel.new(enabled: color)
121
+ end
122
+
123
+ def color?
124
+ @color
125
+ end
126
+
127
+ def border(kind)
128
+ case kind
129
+ when :banner then definition.banner_border
130
+ when :restart then definition.restart_border
131
+ else definition.status_border
132
+ end
133
+ end
134
+
135
+ def box_options(kind = :status)
136
+ {
137
+ border: border(kind),
138
+ enable_color: color?,
139
+ style: { border: { fg: definition.border_color } }
140
+ }
141
+ end
142
+
143
+ def accent(text)
144
+ paint(text, definition.accent_color)
145
+ end
146
+
147
+ def info(text)
148
+ paint(text, definition.info_color)
149
+ end
150
+
151
+ def success(text)
152
+ paint(text, definition.success_color)
153
+ end
154
+
155
+ def warning(text)
156
+ paint(text, definition.warning_color)
157
+ end
158
+
159
+ def error(text)
160
+ paint(text, definition.error_color)
161
+ end
162
+
163
+ def restart(text)
164
+ paint(text, definition.restart_color)
165
+ end
166
+
167
+ def muted(text)
168
+ paint(text, definition.muted_color)
169
+ end
170
+
171
+ def label(text)
172
+ muted(text)
173
+ end
174
+
175
+ def prefix(level = :info)
176
+ text = "[RBWatch]"
177
+ return text unless color?
178
+
179
+ color = case level
180
+ when :success then :green
181
+ when :warning then :yellow
182
+ when :error then :red
183
+ when :restart then :magenta
184
+ else :cyan
185
+ end
186
+ text.colorize(color)
187
+ end
188
+
189
+ def timestamp(time = Time.now)
190
+ muted("[#{time.strftime('%H:%M:%S')}]")
191
+ end
192
+
193
+ def status(text)
194
+ success(text)
195
+ end
196
+
197
+ def command(text)
198
+ info(text)
199
+ end
200
+
201
+ def files(text)
202
+ accent(text)
203
+ end
204
+
205
+ def delay(text)
206
+ accent(text)
207
+ end
208
+
209
+ def pid(text)
210
+ accent(text)
211
+ end
212
+
213
+ def paint(text, color)
214
+ return text.to_s unless color?
215
+
216
+ pastel.public_send(color, text.to_s)
217
+ rescue NoMethodError
218
+ text.to_s
219
+ end
220
+
221
+ private
222
+
223
+ attr_reader :pastel
224
+ end
225
+ end
226
+
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBWatch
4
+ VERSION = "1.0.0"
5
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBWatch
4
+ class Watcher
5
+ Event = Struct.new(:change_set, :snapshot, :first_seen_at, :delivered_at, keyword_init: true)
6
+
7
+ attr_reader :baseline_snapshot, :delay, :poll_interval
8
+
9
+ def initialize(scanner:, delay:, poll_interval: 0.1, clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
10
+ @scanner = scanner
11
+ @delay = delay.to_f
12
+ @poll_interval = poll_interval.to_f
13
+ @clock = clock
14
+ @baseline_snapshot = {}
15
+ @last_observed_snapshot = {}
16
+ @pending_change = nil
17
+ @stopped = false
18
+ end
19
+
20
+ def reset(snapshot = @scanner.scan)
21
+ @baseline_snapshot = snapshot.dup
22
+ @last_observed_snapshot = snapshot.dup
23
+ @pending_change = nil
24
+ self
25
+ end
26
+
27
+ def stop
28
+ @stopped = true
29
+ end
30
+
31
+ def stopped?
32
+ @stopped
33
+ end
34
+
35
+ def step
36
+ current_snapshot = @scanner.scan
37
+ now = @clock.call
38
+ poll_diff = @scanner.diff(@last_observed_snapshot, current_snapshot)
39
+
40
+ if poll_diff.changed?
41
+ if current_snapshot == @baseline_snapshot
42
+ @pending_change = nil
43
+ else
44
+ @pending_change ||= { first_seen_at: now }
45
+ @pending_change[:change_set] = @scanner.diff(@baseline_snapshot, current_snapshot)
46
+ @pending_change[:snapshot] = current_snapshot
47
+ @pending_change[:last_change_at] = now
48
+ end
49
+ elsif @pending_change && now - @pending_change[:last_change_at] >= delay_seconds
50
+ event = Event.new(
51
+ change_set: @pending_change[:change_set],
52
+ snapshot: @pending_change[:snapshot],
53
+ first_seen_at: @pending_change[:first_seen_at],
54
+ delivered_at: now
55
+ )
56
+
57
+ @baseline_snapshot = @pending_change[:snapshot].dup
58
+ @last_observed_snapshot = current_snapshot.dup
59
+ @pending_change = nil
60
+ return event
61
+ end
62
+
63
+ @last_observed_snapshot = current_snapshot.dup
64
+
65
+ if @pending_change && now - @pending_change[:last_change_at] >= delay_seconds
66
+ event = Event.new(
67
+ change_set: @pending_change[:change_set],
68
+ snapshot: @pending_change[:snapshot],
69
+ first_seen_at: @pending_change[:first_seen_at],
70
+ delivered_at: now
71
+ )
72
+
73
+ @baseline_snapshot = @pending_change[:snapshot].dup
74
+ @pending_change = nil
75
+ event
76
+ else
77
+ nil
78
+ end
79
+ end
80
+
81
+ def run
82
+ return enum_for(:run) unless block_given?
83
+
84
+ until stopped?
85
+ if (event = step)
86
+ yield event
87
+ end
88
+ sleep poll_interval
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def delay_seconds
95
+ @delay / 1000.0
96
+ end
97
+ end
98
+ end
data/lib/rbwatch.rb ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RBWatch
4
+ end
5
+
6
+ require_relative "rbwatch/version"
7
+ require_relative "rbwatch/theme"
8
+ require_relative "rbwatch/configuration"
9
+ require_relative "rbwatch/file_scanner"
10
+ require_relative "rbwatch/process_manager"
11
+ require_relative "rbwatch/watcher"
12
+ require_relative "rbwatch/dashboard"
13
+ require_relative "rbwatch/runner"
14
+ require_relative "rbwatch/cli"
15
+
metadata ADDED
@@ -0,0 +1,184 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbwatch
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - RBWatch Contributors
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: colorize
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 1.1.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 1.1.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: pastel
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.8.0
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.8.0
40
+ - !ruby/object:Gem::Dependency
41
+ name: tty-box
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.7.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.7.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: tty-cursor
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.7.1
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: 0.7.1
68
+ - !ruby/object:Gem::Dependency
69
+ name: tty-prompt
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 0.23.1
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.23.1
82
+ - !ruby/object:Gem::Dependency
83
+ name: tty-screen
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.8.2
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: 0.8.2
96
+ - !ruby/object:Gem::Dependency
97
+ name: tty-spinner
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: 0.9.3
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: 0.9.3
110
+ - !ruby/object:Gem::Dependency
111
+ name: tty-table
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: 0.12.0
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: 0.12.0
124
+ - !ruby/object:Gem::Dependency
125
+ name: rspec
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '3.13'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '3.13'
138
+ description: RBWatch watches Ruby project files, restarts processes on changes, and
139
+ presents a polished terminal UI.
140
+ email:
141
+ - opensource@example.com
142
+ executables:
143
+ - rbw
144
+ - rbwatch
145
+ extensions: []
146
+ extra_rdoc_files: []
147
+ files:
148
+ - README.md
149
+ - bin/rbw
150
+ - bin/rbwatch
151
+ - exe/rbw
152
+ - exe/rbwatch
153
+ - lib/rbwatch.rb
154
+ - lib/rbwatch/cli.rb
155
+ - lib/rbwatch/configuration.rb
156
+ - lib/rbwatch/dashboard.rb
157
+ - lib/rbwatch/file_scanner.rb
158
+ - lib/rbwatch/process_manager.rb
159
+ - lib/rbwatch/runner.rb
160
+ - lib/rbwatch/theme.rb
161
+ - lib/rbwatch/version.rb
162
+ - lib/rbwatch/watcher.rb
163
+ homepage: https://github.com/cavalheri/rbwatch
164
+ licenses:
165
+ - MIT
166
+ metadata: {}
167
+ rdoc_options: []
168
+ require_paths:
169
+ - lib
170
+ required_ruby_version: !ruby/object:Gem::Requirement
171
+ requirements:
172
+ - - ">="
173
+ - !ruby/object:Gem::Version
174
+ version: '3.1'
175
+ required_rubygems_version: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ version: '0'
180
+ requirements: []
181
+ rubygems_version: 4.0.11
182
+ specification_version: 4
183
+ summary: A delightful Ruby file watcher and process reloader
184
+ test_files: []