foruiman 0.1.2 → 0.3.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 +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +21 -4
- data/docs/ARCHITECTURE.md +24 -6
- data/docs/COMPATIBILITY.md +62 -14
- data/docs/RAILS.md +19 -2
- data/lib/foruiman/ansi.rb +8 -3
- data/lib/foruiman/cli.rb +41 -20
- data/lib/foruiman/configuration.rb +101 -0
- data/lib/foruiman/engine.rb +135 -21
- data/lib/foruiman/env.rb +5 -2
- data/lib/foruiman/output.rb +22 -4
- data/lib/foruiman/output_line.rb +100 -0
- data/lib/foruiman/process.rb +1 -1
- data/lib/foruiman/tui/application.rb +78 -3
- data/lib/foruiman/tui/input_line.rb +217 -0
- data/lib/foruiman/tui/keyboard.rb +1 -1
- data/lib/foruiman/tui/log_formatter.rb +2 -0
- data/lib/foruiman/tui/renderer.rb +48 -11
- data/lib/foruiman/tui/state.rb +12 -1
- data/lib/foruiman/tui/theme.rb +2 -2
- data/lib/foruiman/version.rb +1 -1
- metadata +5 -2
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Foruiman::TUI
|
|
4
|
+
class InputLine
|
|
5
|
+
# Leave room for the newline in the child's canonical terminal buffer.
|
|
6
|
+
MAX_BYTES = 4095
|
|
7
|
+
HISTORY_SIZE = 100
|
|
8
|
+
SEQUENCES = {
|
|
9
|
+
"\e[A" => :history_previous, "\e[B" => :history_next,
|
|
10
|
+
"\e[C" => :right, "\e[D" => :left,
|
|
11
|
+
"\e[H" => :home, "\e[F" => :end,
|
|
12
|
+
"\e[1~" => :home, "\e[4~" => :end, "\e[3~" => :delete
|
|
13
|
+
}.freeze
|
|
14
|
+
CONTROLS = {
|
|
15
|
+
1 => :home, 3 => :interrupt, 4 => :eof, 5 => :end, 8 => :backspace,
|
|
16
|
+
10 => :submit, 13 => :submit, 18 => :history_previous, 21 => :clear, 23 => :delete_word,
|
|
17
|
+
24 => :detach, 127 => :backspace
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :text, :cursor
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@text = +""
|
|
24
|
+
@cursor = 0
|
|
25
|
+
@bytes = +"".b
|
|
26
|
+
@history = []
|
|
27
|
+
@history_index = nil
|
|
28
|
+
@draft = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def feed(bytes)
|
|
32
|
+
bytes, detach, = bytes.b.partition("\x18")
|
|
33
|
+
@bytes << bytes
|
|
34
|
+
events = []
|
|
35
|
+
until @bytes.empty?
|
|
36
|
+
if @bytes.start_with?("\e")
|
|
37
|
+
if @bytes.bytesize > 64 && incomplete_escape?
|
|
38
|
+
@bytes.clear
|
|
39
|
+
break
|
|
40
|
+
end
|
|
41
|
+
break if incomplete_escape?
|
|
42
|
+
|
|
43
|
+
sequence = SEQUENCES.keys.find { |candidate| @bytes.start_with?(candidate) }
|
|
44
|
+
if sequence
|
|
45
|
+
edit(SEQUENCES.fetch(sequence))
|
|
46
|
+
@bytes.slice!(0, sequence.bytesize)
|
|
47
|
+
else
|
|
48
|
+
match = @bytes.match(%r{\A\e(?:\[[0-?]*[ -/]*[@-~]|O.|.)}m)
|
|
49
|
+
@bytes.slice!(0, match ? match[0].bytesize : 1)
|
|
50
|
+
end
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
byte = @bytes.getbyte(0)
|
|
55
|
+
if @after_cr && byte == 10
|
|
56
|
+
@bytes.slice!(0, 1)
|
|
57
|
+
@after_cr = false
|
|
58
|
+
next
|
|
59
|
+
end
|
|
60
|
+
@after_cr = byte == 13
|
|
61
|
+
if CONTROLS.key?(byte)
|
|
62
|
+
@bytes.slice!(0, 1)
|
|
63
|
+
event = edit(CONTROLS.fetch(byte))
|
|
64
|
+
events << event if event
|
|
65
|
+
elsif byte < 32
|
|
66
|
+
@bytes.slice!(0, 1)
|
|
67
|
+
else
|
|
68
|
+
character = next_character
|
|
69
|
+
break unless character
|
|
70
|
+
|
|
71
|
+
insert(character)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
unless detach.empty?
|
|
75
|
+
@bytes.clear
|
|
76
|
+
clear
|
|
77
|
+
events << :detach
|
|
78
|
+
end
|
|
79
|
+
events
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def clear
|
|
83
|
+
replace("")
|
|
84
|
+
@history_index = nil
|
|
85
|
+
@draft = nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def edit(action)
|
|
91
|
+
case action
|
|
92
|
+
when :left then @cursor -= 1 if cursor.positive?
|
|
93
|
+
when :right then @cursor += 1 if cursor < graphemes.size
|
|
94
|
+
when :home then @cursor = 0
|
|
95
|
+
when :end then @cursor = graphemes.size
|
|
96
|
+
when :backspace then remove(cursor - 1) if cursor.positive?
|
|
97
|
+
when :delete then remove(cursor) if cursor < graphemes.size
|
|
98
|
+
when :delete_word then delete_word
|
|
99
|
+
when :clear then clear
|
|
100
|
+
when :history_previous then history_previous
|
|
101
|
+
when :history_next then history_next
|
|
102
|
+
when :submit then return submit
|
|
103
|
+
when :interrupt
|
|
104
|
+
clear
|
|
105
|
+
return :interrupt
|
|
106
|
+
when :eof
|
|
107
|
+
return :eof if text.empty?
|
|
108
|
+
|
|
109
|
+
remove(cursor) if cursor < graphemes.size
|
|
110
|
+
when :detach
|
|
111
|
+
clear
|
|
112
|
+
return :detach
|
|
113
|
+
end
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def submit
|
|
118
|
+
line = text.dup
|
|
119
|
+
@history << line.freeze unless line.empty? || @history.last == line
|
|
120
|
+
@history.shift if @history.size > HISTORY_SIZE
|
|
121
|
+
clear
|
|
122
|
+
[:submit, line]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def insert(character)
|
|
126
|
+
return if text.bytesize + character.bytesize > MAX_BYTES
|
|
127
|
+
return if character.match?(/[\u0080-\u009f]/)
|
|
128
|
+
|
|
129
|
+
if character.ascii_only? && cursor == text.length
|
|
130
|
+
@text << character
|
|
131
|
+
@cursor += 1
|
|
132
|
+
@history_index = nil
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
parts = graphemes
|
|
137
|
+
parts.insert(cursor, character)
|
|
138
|
+
@text = parts.join
|
|
139
|
+
@cursor = [cursor + 1, graphemes.size].min
|
|
140
|
+
@history_index = nil
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def remove(index)
|
|
144
|
+
parts = graphemes
|
|
145
|
+
parts.delete_at(index)
|
|
146
|
+
@text = parts.join
|
|
147
|
+
@cursor -= 1 if index < cursor
|
|
148
|
+
@history_index = nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def delete_word
|
|
152
|
+
parts = graphemes
|
|
153
|
+
index = cursor
|
|
154
|
+
index -= 1 while index.positive? && parts[index - 1].match?(/\s/)
|
|
155
|
+
index -= 1 while index.positive? && !parts[index - 1].match?(/\s/)
|
|
156
|
+
parts.slice!(index...cursor)
|
|
157
|
+
@text = parts.join
|
|
158
|
+
@cursor = index
|
|
159
|
+
@history_index = nil
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def history_previous
|
|
163
|
+
return if @history.empty?
|
|
164
|
+
|
|
165
|
+
@draft = text.dup unless @history_index
|
|
166
|
+
@history_index = [(@history_index || @history.size) - 1, 0].max
|
|
167
|
+
replace(@history.fetch(@history_index))
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def history_next
|
|
171
|
+
return unless @history_index
|
|
172
|
+
|
|
173
|
+
@history_index += 1
|
|
174
|
+
if @history_index >= @history.size
|
|
175
|
+
@history_index = nil
|
|
176
|
+
replace(@draft.to_s)
|
|
177
|
+
@draft = nil
|
|
178
|
+
else
|
|
179
|
+
replace(@history.fetch(@history_index))
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def replace(value)
|
|
184
|
+
@text = +value
|
|
185
|
+
@cursor = graphemes.size
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def graphemes
|
|
189
|
+
text.scan(/\X/)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def next_character
|
|
193
|
+
size = utf8_size(@bytes.getbyte(0))
|
|
194
|
+
return @bytes.slice!(0, 1).force_encoding(Encoding::UTF_8).scrub unless size
|
|
195
|
+
unless @bytes.byteslice(1, size - 1).bytes.all? { |byte| byte.between?(128, 191) }
|
|
196
|
+
return @bytes.slice!(0, 1).force_encoding(Encoding::UTF_8).scrub
|
|
197
|
+
end
|
|
198
|
+
return if @bytes.bytesize < size
|
|
199
|
+
|
|
200
|
+
@bytes.slice!(0, size).force_encoding(Encoding::UTF_8).scrub
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def utf8_size(byte)
|
|
204
|
+
case byte
|
|
205
|
+
when 0..127 then 1
|
|
206
|
+
when 194..223 then 2
|
|
207
|
+
when 224..239 then 3
|
|
208
|
+
when 240..244 then 4
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def incomplete_escape?
|
|
213
|
+
@bytes == "\e" || @bytes == "\e[" || @bytes == "\eO" ||
|
|
214
|
+
(@bytes.start_with?("\e[") && !@bytes.match?(%r{\A\e\[[0-?]*[ -/]*[@-~]}))
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -12,7 +12,7 @@ module Foruiman::TUI
|
|
|
12
12
|
"\t" => :next, "h" => :previous, "l" => :next, "k" => :up, "j" => :down,
|
|
13
13
|
"g" => :home, "G" => :end, "f" => :follow, " " => :toggle_follow,
|
|
14
14
|
"r" => :restart, "R" => :restart_all, "s" => :stop, "S" => :stop_all,
|
|
15
|
-
"?" => :help, "q" => :quit, "\x03" => :quit,
|
|
15
|
+
"i" => :input, "?" => :help, "q" => :quit, "\x03" => :quit,
|
|
16
16
|
"\x15" => :page_up, "\x04" => :page_down
|
|
17
17
|
}.freeze
|
|
18
18
|
|
|
@@ -30,6 +30,7 @@ module Foruiman::TUI
|
|
|
30
30
|
|
|
31
31
|
def stream_style(stream)
|
|
32
32
|
case stream
|
|
33
|
+
when :stdin then ["in ", :cyan]
|
|
33
34
|
when :stderr then ["err", :red]
|
|
34
35
|
when :lifecycle then ["sys", :amber]
|
|
35
36
|
else ["out", :faint]
|
|
@@ -38,6 +39,7 @@ module Foruiman::TUI
|
|
|
38
39
|
|
|
39
40
|
def body_color(stream)
|
|
40
41
|
case stream
|
|
42
|
+
when :stdin then :cyan
|
|
41
43
|
when :stderr then :red
|
|
42
44
|
when :lifecycle then :muted
|
|
43
45
|
else :text
|
|
@@ -16,7 +16,8 @@ module Foruiman::TUI
|
|
|
16
16
|
[nil, "f / G / End", "Follow newest output", nil],
|
|
17
17
|
[nil, "Space", "Pause / resume following", nil],
|
|
18
18
|
["⚙ PROCESSES", "r / R", "Restart selected / all", "r on all restarts all"],
|
|
19
|
-
[nil, "s / S", "
|
|
19
|
+
[nil, "s / S", "Start/stop selected / stop all", "s on all stops all"],
|
|
20
|
+
["⌨ INPUT", "i", "Edit a command; Enter sends", "Ctrl-X returns to Foruiman"],
|
|
20
21
|
["◇ SESSION", "? / Escape", "Close help", nil],
|
|
21
22
|
[nil, "q / Ctrl-C", "Stop processes and quit", nil]
|
|
22
23
|
].freeze
|
|
@@ -30,16 +31,16 @@ module Foruiman::TUI
|
|
|
30
31
|
rows >= 14 && columns >= 60
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def self.log_height(rows:, columns:)
|
|
34
|
+
def self.log_height(rows:, columns:, input: false)
|
|
34
35
|
return 0 if rows < 6 || columns < 24
|
|
35
36
|
|
|
36
|
-
rows - (expanded?(rows: rows, columns: columns) ? 7 : 5)
|
|
37
|
+
rows - (expanded?(rows: rows, columns: columns) ? 7 : 5) - (input && rows >= 8 ? 2 : 0)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
def render(state, engine, rows:, columns:)
|
|
40
41
|
@width = [columns - 1, 1].max
|
|
41
42
|
@inside = [@width - 4, 0].max
|
|
42
|
-
@height = self.class.log_height(rows: rows, columns: columns)
|
|
43
|
+
@height = self.class.log_height(rows: rows, columns: columns, input: state.input?)
|
|
43
44
|
return tiny_frame(state, engine, rows) if @height.zero?
|
|
44
45
|
|
|
45
46
|
lines = [header(engine)]
|
|
@@ -54,6 +55,7 @@ module Foruiman::TUI
|
|
|
54
55
|
lines << log_header(state, engine)
|
|
55
56
|
lines.concat(state.help ? help_rows : log_rows(state, engine))
|
|
56
57
|
lines << log_footer(state, engine)
|
|
58
|
+
lines.concat(input_rows(state)) if state.input? && rows >= 8
|
|
57
59
|
lines << controls(state, engine)
|
|
58
60
|
frame(lines, rows)
|
|
59
61
|
end
|
|
@@ -157,7 +159,9 @@ module Foruiman::TUI
|
|
|
157
159
|
else
|
|
158
160
|
engine.state(state.name).status == :running
|
|
159
161
|
end
|
|
160
|
-
mode = if
|
|
162
|
+
mode = if state.input?
|
|
163
|
+
@theme.paint(" ▶ INPUT ", :cyan)
|
|
164
|
+
elsif !state.viewport.following
|
|
161
165
|
@theme.paint(" Ⅱ PAUSED ", :amber)
|
|
162
166
|
elsif active
|
|
163
167
|
@theme.paint(" ● LIVE ", :green)
|
|
@@ -244,28 +248,61 @@ module Foruiman::TUI
|
|
|
244
248
|
end
|
|
245
249
|
|
|
246
250
|
def controls(state, engine)
|
|
251
|
+
if state.input?
|
|
252
|
+
left = @theme.paint(" Enter send · ↑↓ history · Ctrl-C interrupt", :muted)
|
|
253
|
+
right = @theme.paint(" Ctrl-X back ", :amber, bold: true)
|
|
254
|
+
return distribute(left, right, @width)
|
|
255
|
+
end
|
|
256
|
+
|
|
247
257
|
quit = @theme.paint(" q ", :accent, bold: true) + @theme.paint(" × quit ", :muted)
|
|
248
258
|
left = if engine.shutting_down?
|
|
249
|
-
|
|
259
|
+
seconds = format("%g", engine.term_timeout)
|
|
260
|
+
@theme.paint(" ◌ Stopping process groups · TERM → KILL after #{seconds}s", :amber)
|
|
250
261
|
elsif state.feedback
|
|
251
262
|
@theme.paint(" #{state.feedback}", :amber)
|
|
252
263
|
elsif state.help
|
|
253
264
|
@theme.paint(" ? / Escape close help", :muted)
|
|
254
265
|
else
|
|
255
|
-
shortcuts(state)
|
|
266
|
+
shortcuts(state, engine)
|
|
256
267
|
end
|
|
257
268
|
distribute(left, quit, @width)
|
|
258
269
|
end
|
|
259
270
|
|
|
260
|
-
def shortcuts(state)
|
|
271
|
+
def shortcuts(state, engine)
|
|
261
272
|
restart = ["r", state.name == "all" ? "↻ restart all" : "↻ restart"]
|
|
262
|
-
|
|
263
|
-
|
|
273
|
+
toggle = if state.name == "all"
|
|
274
|
+
["s", "■ stop all"]
|
|
275
|
+
elsif process_active?(engine.state(state.name))
|
|
276
|
+
["s", "■ stop"]
|
|
277
|
+
else
|
|
278
|
+
["s", "▶ start"]
|
|
279
|
+
end
|
|
280
|
+
pairs = [%w[Tab switch], ["↑↓", "scroll"], %w[f follow], restart,
|
|
281
|
+
toggle, %w[i input], ["?", "help"]]
|
|
282
|
+
pairs = [restart, toggle, %w[i input], ["?", "help"]] if @width < 85
|
|
264
283
|
pairs.map do |key, label|
|
|
265
284
|
@theme.paint(" #{key} ", :accent, bold: true) + @theme.paint(" #{label} ", :muted)
|
|
266
285
|
end.join
|
|
267
286
|
end
|
|
268
287
|
|
|
288
|
+
def process_active?(entry)
|
|
289
|
+
entry.pgid || %i[running restarting stopping].include?(entry.status)
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def input_rows(state)
|
|
293
|
+
editor = state.input_line
|
|
294
|
+
parts = editor.text.scan(/\X/)
|
|
295
|
+
before = parts.take(editor.cursor)
|
|
296
|
+
current = parts[editor.cursor] || " "
|
|
297
|
+
after = parts.drop(editor.cursor + 1).join
|
|
298
|
+
available = [@inside - 5, 1].max
|
|
299
|
+
before.shift while !before.empty? && Text.width(before.join + current) > available
|
|
300
|
+
draft = @theme.paint("❯ ", :cyan, bold: true) + before.join +
|
|
301
|
+
@theme.paint(current,
|
|
302
|
+
selected: true) + Text.clip(after, [available - Text.width(before.join + current), 0].max)
|
|
303
|
+
[border(@theme.paint(" ⌨ Input to #{state.input_target} ", :cyan, bold: true)), panel(draft)]
|
|
304
|
+
end
|
|
305
|
+
|
|
269
306
|
def border(left, right = "", bottom: false)
|
|
270
307
|
corners = bottom ? %w[╰ ╯] : %w[╭ ╮]
|
|
271
308
|
middle = distribute(left, right, @inside, fill: "─")
|
|
@@ -290,7 +327,7 @@ module Foruiman::TUI
|
|
|
290
327
|
title = @theme.paint(" FORUIMAN", :accent, bold: true)
|
|
291
328
|
message = engine.shutting_down? ? " Stopping…" : " #{state.name} · enlarge terminal"
|
|
292
329
|
lines = [title, @theme.paint(message, :muted)]
|
|
293
|
-
lines[rows - 1] = @theme.paint(" q quit", :accent) if rows > 2
|
|
330
|
+
lines[rows - 1] = @theme.paint(state.input? ? " Ctrl-X back" : " q quit", :accent) if rows > 2
|
|
294
331
|
frame(lines, rows)
|
|
295
332
|
end
|
|
296
333
|
|
data/lib/foruiman/tui/state.rb
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "viewport"
|
|
4
|
+
require_relative "input_line"
|
|
4
5
|
|
|
5
6
|
module Foruiman::TUI
|
|
6
7
|
class State
|
|
7
8
|
attr_reader :tabs, :selected, :viewports
|
|
8
|
-
attr_accessor :help, :feedback
|
|
9
|
+
attr_accessor :help, :feedback, :input_target
|
|
9
10
|
|
|
10
11
|
def initialize(names)
|
|
11
12
|
@tabs = [*names, "all"].freeze
|
|
@@ -13,6 +14,8 @@ module Foruiman::TUI
|
|
|
13
14
|
@viewports = tabs.to_h { |name| [name, Viewport.new] }
|
|
14
15
|
@help = false
|
|
15
16
|
@feedback = nil
|
|
17
|
+
@input_target = nil
|
|
18
|
+
@input_lines = names.to_h { |name| [name, InputLine.new] }
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def name
|
|
@@ -23,6 +26,14 @@ module Foruiman::TUI
|
|
|
23
26
|
viewports.fetch(name)
|
|
24
27
|
end
|
|
25
28
|
|
|
29
|
+
def input?
|
|
30
|
+
!input_target.nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def input_line
|
|
34
|
+
@input_lines.fetch(input_target || name)
|
|
35
|
+
end
|
|
36
|
+
|
|
26
37
|
def select(index)
|
|
27
38
|
@selected = index if index.between?(0, tabs.size - 1)
|
|
28
39
|
end
|
data/lib/foruiman/tui/theme.rb
CHANGED
|
@@ -16,8 +16,8 @@ module Foruiman::TUI
|
|
|
16
16
|
stopped: :muted, exited: :muted, failed: :red
|
|
17
17
|
}.freeze
|
|
18
18
|
STATUS_MARKS = {
|
|
19
|
-
pending: "○", running: "
|
|
20
|
-
stopped: "■", exited: "✓", failed: "
|
|
19
|
+
pending: "○", running: "▶", restarting: "↻", stopping: "◼",
|
|
20
|
+
stopped: "■", exited: "✓", failed: "✕"
|
|
21
21
|
}.freeze
|
|
22
22
|
|
|
23
23
|
attr_reader :enabled
|
data/lib/foruiman/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foruiman
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foruiman contributors
|
|
@@ -70,16 +70,19 @@ files:
|
|
|
70
70
|
- lib/foruiman.rb
|
|
71
71
|
- lib/foruiman/ansi.rb
|
|
72
72
|
- lib/foruiman/cli.rb
|
|
73
|
+
- lib/foruiman/configuration.rb
|
|
73
74
|
- lib/foruiman/diagnostics.rb
|
|
74
75
|
- lib/foruiman/engine.rb
|
|
75
76
|
- lib/foruiman/env.rb
|
|
76
77
|
- lib/foruiman/log_store.rb
|
|
77
78
|
- lib/foruiman/output.rb
|
|
79
|
+
- lib/foruiman/output_line.rb
|
|
78
80
|
- lib/foruiman/plain.rb
|
|
79
81
|
- lib/foruiman/process.rb
|
|
80
82
|
- lib/foruiman/procfile.rb
|
|
81
83
|
- lib/foruiman/ring.rb
|
|
82
84
|
- lib/foruiman/tui/application.rb
|
|
85
|
+
- lib/foruiman/tui/input_line.rb
|
|
83
86
|
- lib/foruiman/tui/keyboard.rb
|
|
84
87
|
- lib/foruiman/tui/log_formatter.rb
|
|
85
88
|
- lib/foruiman/tui/renderer.rb
|
|
@@ -110,7 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
110
113
|
- !ruby/object:Gem::Version
|
|
111
114
|
version: '0'
|
|
112
115
|
requirements: []
|
|
113
|
-
rubygems_version: 4.0.
|
|
116
|
+
rubygems_version: 4.0.20
|
|
114
117
|
specification_version: 4
|
|
115
118
|
summary: A Procfile supervisor with process tabs, bounded logs, and isolated restarts
|
|
116
119
|
test_files: []
|